Skip to content

Latest commit

 

History

History
371 lines (345 loc) · 9.72 KB

SpringBoot_SPA_OAuth2_Example.md

File metadata and controls

371 lines (345 loc) · 9.72 KB
presentation
theme width height controls enableSpeakerNotes
beige.css
1024
900
true
true

1. Register Your API and Web App On AAD

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

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

<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);
    }
  }
}

3. Setup OAuth2 Client In Web App

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();
  }
};

-End-