diff --git a/SpringBoot_SPA_OAuth2_Example b/SpringBoot_SPA_OAuth2_Example
new file mode 100644
index 0000000..0fc89a1
--- /dev/null
+++ b/SpringBoot_SPA_OAuth2_Example
@@ -0,0 +1,371 @@
+---
+presentation:
+ theme: beige.css
+ width: 1024
+ height: 900
+ controls: true
+ enableSpeakerNotes: true
+---
+
+## 1. Register Your API and Web App On AAD
+
+
+Follow the [official manual](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-configure-app-expose-web-apis) to expose your API.
+
+Key points:
+- Copy the `Application ID URI`.
+
+
+Follow the [official manual](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-configure-app-access-web-apis) 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
+
+
+## 2. Configure OAuth2 Token Validator on Backend Side
+
+
+Assuming you are using Spring Boot and Spring Security
+Add `spring-boot-starter-parent` in pom.xml
+```xml
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.1.6.RELEASE
+
+
+```
+
+
+Add dependencies
+```xml
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.security.oauth
+ spring-security-oauth2
+ 2.2.4.RELEASE
+
+
+ org.springframework.security
+ spring-security-oauth2-jose
+
+
+ org.springframework.security
+ spring-security-oauth2-resource-server
+
+
+ org.springframework.security.oauth.boot
+ spring-security-oauth2-autoconfigure
+ 2.1.5.RELEASE
+
+
+```
+
+Spring Security Configuration
+```java
+@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
+```java
+@Bean
+public JwtDecoder jwtDecoder() {
+ NimbusJwtDecoder decoder = (NimbusJwtDecoder)JwtDecoders.fromOidcIssuerLocation("https://sts.windows.net/" + this.tenantId + "/");
+ // verify expiry time and audience in the token
+ DelegatingOAuth2TokenValidator 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 {
+ 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);
+ }
+ }
+}
+```
+
+
+## 3. Setup OAuth2 Client In Web App
+
+
+Add _MSAL_ dependency in `package.json`
+```json
+"dependencies": {
+ "axios": "^0.19.2",
+ "msal": "^1.2.2",
+ ...
+}
+```
+
+
+Configure MSAL client
+
+```js
+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
+```js
+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
+```js
+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
+```js
+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
+```js
+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
+```js
+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.} 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
+```js
+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();
+ }
+};
+```
+
+
+# -End-