presentation | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
Follow the official manual to expose your API.
Key points:
- Copy the
Application ID URI
.
Follow the official manual to register the web app.
Key points:
- Enable
Implicit Flow
for the web app - Set the
Redirect Url
to your web app home page - Add API to the permission list
Assuming you are using Spring Boot and Spring Security
Add spring-boot-starter-parent
in pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
Add dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>
</dependencies>
Spring Security Configuration
@Configuration
@EnableWebSecurity
@EnableResourceServer
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// enable JWT token validation
.oauth2ResourceServer()
.jwt()
.and()
.authenticationEntryPoint(aadEntryPoint);
}
}
// the customized entry point
@Component
public class AADLoginEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
/*
* On API side, we are not responsible to handle user authentication.
* So we simply response 401 error here for requests with invalid token.
*/
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Unauthorized");
}
}
Configure JWT token validator
@Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder decoder = (NimbusJwtDecoder)JwtDecoders.fromOidcIssuerLocation("https://sts.windows.net/" + this.tenantId + "/");
// verify expiry time and audience in the token
DelegatingOAuth2TokenValidator<Jwt> tokenValidators = new DelegatingOAuth2TokenValidator<>(
// the maximum time skew allowed if token is found expired
new JwtTimestampValidator(Duration.ofSeconds(60)),
new AudienceValidator());
decoder.setJwtValidator(tokenValidators);
return decoder;
}
class AudienceValidator implements OAuth2TokenValidator<Jwt> {
OAuth2Error error = new OAuth2Error("invalid_token", "Invalid token.", null);
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
// resourceId is the Application ID URI of registered api in AAD previously
if (null != jwt.getAudience() && jwt.getAudience().stream().anyMatch(a -> a != null && a.equalsIgnoreCase(resourceId))) {
return OAuth2TokenValidatorResult.success();
} else {
return OAuth2TokenValidatorResult.failure(error);
}
}
}
Add MSAL dependency in package.json
"dependencies": {
"axios": "^0.19.2",
"msal": "^1.2.2",
...
}
Configure MSAL client
import * as Msal from 'msal';
const msalConfig = {
auth: {
clientId: '[App id of web app registered in AAD]',
redirectUri: '[redirect url of the web app registered in AAD]'
},
cache: {
cacheLocation: 'sessionStorage'
}
};
Sign in user
signIn() {
if (!this.msalInstance) {
this.initialize();
}
var loginRequest = {
};
return new Promise((resolve, reject) => {
if (!this.msalInstance.getAccount()) {
this.msalInstance
.loginRedirect(loginRequest)
.then(resp => {
resolve(resp);
})
.catch(err => {
reject(err);
});
}
resolve(this.msalInstance.getAccount());
});
},
Call the signIn function while initializing the page
auth.signIn().then(function(token) { // this is the id token
var user = {
userId: token.userName,
userName: token.name
}
// do something after user sign in, e.g. render your page
})
Before call api, you need acquire access token
acquireToken() {
if (!this.msalInstance) {
this.initialize();
}
return new Promise((resolve, reject) => {
if (this.msalInstance.getAccount()) {
var tokenRequest = {
// resourceId is the App ID URI of the registered api in AAD
scopes: [resourceId + '/.default']
};
this.msalInstance
.acquireTokenSilent(tokenRequest)
.then(r => {
resolve(r.accessToken);
})
.catch(err => {
// could also check if err instance of InteractionRequiredAuthError if you can import the class.
if (err.name === 'InteractionRequiredAuthError') {
return this.msalInstance
.acquireTokenRedirect(tokenRequest)
.then(r => {
resolve(r.accessToken);
})
.catch(err => {
reject(err);
});
}
});
} else {
this.msalInstance.loginRedirect();
}
});
},
then call the api with the access token
import axios from 'axios'
import auth from '../auth'
// request interceptors
axios.interceptors.request.use(
async function (config) {
config.headers = config.headers || {};
var token = await auth.acquireToken();
config.headers.Authorization = "Bearer " + token;
return config;
},
function(err) {
// Do something with request error
return Promise.reject(err);
}
);
// api request example
axios.get('http://localhost:8080/api/v1/users')
.then()
.catch()
Complete auth.js for your reference
import * as Msal from 'msal';
import config from './config';
const msalConfig = {
auth: {
clientId: config.oauth.clientId,
redirectUri: config.oauth.redirectUri
},
cache: {
cacheLocation: 'sessionStorage'
}
};
export default {
msalInstance: null,
initialize() {
// msal instance
this.msalInstance = new Msal.UserAgentApplication(msalConfig);
this.msalInstance.handleRedirectCallback((err) => {
if (err) {
console.log(err)
}
});
},
/**
* @return {Promise.<String>} A promise that resolves to an access token for resource access
*/
acquireToken() {
if (!this.msalInstance) {
this.initialize();
}
return new Promise((resolve, reject) => {
if (this.msalInstance.getAccount()) {
var tokenRequest = {
scopes: [config.api.resourceId + '/.default']
};
this.msalInstance
.acquireTokenSilent(tokenRequest)
.then(r => {
resolve(r.accessToken);
})
.catch(err => {
// could also check if err instance of InteractionRequiredAuthError if you can import the class.
if (err.name === 'InteractionRequiredAuthError') {
return this.msalInstance
.acquireTokenRedirect(tokenRequest)
.then(r => {
resolve(r.accessToken);
})
.catch(err => {
reject(err);
});
}
});
} else {
this.msalInstance.loginRedirect();
}
});
},
// to be continue...
Continued
isAuthenticated() {
return this.msalInstance && this.msalInstance.getAccount()
},
/**
* Sign in user.
*/
signIn() {
if (!this.msalInstance) {
this.initialize();
}
var loginRequest = {
};
return new Promise((resolve, reject) => {
if (!this.msalInstance.getAccount()) {
this.msalInstance
.loginRedirect(loginRequest)
.then(resp => {
resolve(resp);
})
.catch(err => {
reject(err);
});
}
resolve(this.msalInstance.getAccount());
});
},
signOut() {
this.msalInstance.logout();
}
};