Spring Security and OAUTH2 with Azure Active Directory

Azure Active Directory (Azure AD) uses OAuth 2.0 to enable you to authorize access to web applications and web APIs in your Azure AD tenant. In Azure Active Directory (Azure AD), a tenant is representative of an organization. It is a dedicated instance of the Azure AD service that an organization receives and owns when it signs up for a Microsoft cloud service such as Azure. 

Step-1 : To Implement OAUTH2 with Azure AD first of all you must get a tenant on Azure AD.

How to get an Azure Active Directory tenant

https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-howto-tenant

Step-2: Register the new app with Azure AD

To set up the app to authenticate users, first register it in your tenant by doing the following:

  1. Sign in to the Azure portal.
  2. On the top bar, click your account name. Under the Directorylist, select the Active Directory tenant where you want to register the app.
  3. Click More Servicesin the left pane, and then select Azure Active Directory.
  4. Click App registrations, and then select Add.
  5. Follow the prompts to create a Web Application and/or WebAPI.
  6. After you’ve completed the registration, Azure AD assigns the app a unique application ID. Copy the value from the app page to use in the next sections.
  1. From the Settings -> Properties page for your application, update the App ID URI. The App ID URI is a unique identifier for the app. The naming convention is https://<tenant-domain>/<app-name> (for example, http://localhost:8080/ ResourceApp /).
  1. To Get Tenant ID Click App registrations, and then select Endpoints

When you are in the portal for the app, create and copy a key for the app on the Settings page. You’ll need the key shortly.

Step-3: Get a new or existing application in which you want to secure your REST endpoints with oauth2 token.

Here I am using a Spring Boot App for demonstration purpose.

Add following dependencies in your pom.xml.

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.2.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
    <groupId>com.microsoft.azure</groupId>
    <artifactId>adal4j</artifactId>
    <version>1.1.1</version>
</dependency>
<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>oauth2-oidc-sdk</artifactId>
    <version>4.5</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>

Add following properties in application.properties

security.oauth2.client.clientId=06a8ddb3-79b1-4826-a61c-4d304
security.oauth2.client.clientSecret=cxZ7dUMWh7tT6VrFQ4uX1y+2uD8QqlCHS+A2/ygRdD0=
security.oauth2.client.tenant=ceddc98e-d9e8-4242-bd71-8a08a48
security.oauth2.client.accessTokenUri=https://login.microsoftonline.com/ceddc98e-d9e8-4242-bd71-8a08a48/oauth2/token
security.oauth2.client.userAuthorizationUri=https://login.microsoftonline.com/ ceddc98e-d9e8-4242-bd71-8a08a48/oauth2/authorize
security.oauth2.client.authority=https://login.microsoftonline.com/
security.oauth2.client.resource=https://graph.windows.net/
security.oauth2.resource.userInfoUri=https://graph.windows.net/me?api-version=1.6

Client Id is obtained as Application ID Object under App Registration

To Get Client Secret Click Azure Active Directory–>App Registration–>App–>Settings–>Keys

To Get Tenant ID Click Azure Active Directory–>Properties

Directory ID is Tenant ID so update it

Now Click Azure Active Directory–>App Registrations–>End Points

Update below for property security.oauth2.client.accessTokenUri =

Update below for property security.oauth2.client.userAuthorizationUri =

 

And let other properties remain as it is.

Now Add following configuration classes in your security configuration package.

By using @EnableGlobalMethodSecurity we can easily secure our methods with Java configuration.

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return new OAuth2MethodSecurityExpressionHandler();
    }
}

By Using @EnableResourceServer we can make application to be configured as resource server. ResourceServerConfigurerAdapter provide configurations for defining spring security configuration concerns.

@Configuration
@EnableResourceServer
public class OAuth2Config extends ResourceServerConfigurerAdapter {

    private TokenExtractor tokenExtractor = new BearerTokenExtractor();

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(new OncePerRequestFilter() {
            @Override
            protected void doFilterInternal(HttpServletRequest request,
                                            HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
                // We don't want to allow access to a resource with no token so clear
                // the security context in case it is actually an OAuth2Authentication
                if (tokenExtractor.extract(request) == null) {
                    SecurityContextHolder.clearContext();
                }
                filterChain.doFilter(request, response);
            }
        }, AbstractPreAuthenticatedProcessingFilter.class);
        http.
                sessionManagement().
                sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().
                authorizeRequests().antMatchers(
                "/access_token", "/refresh/access_token", "/auth_server/config",
                "/index", "/", "/index**", "/resources/**", "/css/**", "/welcome**",
                "/fonts/**", "/icons/**", "/js/**", "/libs/**", "/img/**"
        ).permitAll().
                anyRequest().authenticated();
    }

    @Bean
    public UserInfoTokenServices remoteTokenServices(final @Value("${security.oauth2.resource.userInfoUri}") String checkTokenUrl,
                                                     final @Value("${security.oauth2.client.clientId}") String clientId) {
        final UserInfoTokenServices remoteTokenServices = new UserInfoTokenServices(checkTokenUrl, clientId);
        return remoteTokenServices;
    }
}

Resource server must provide a bean for ResourceServerTokenServices. UserInfoTokenServices does the same purpose here so that request access at resource server should be validated from authorization server.

Now in our application there will be two types of resources

    • Unsecured Resources
    • Secured Resources

 

Unsecured Resources: are those which can be accessed in application with no need of access token. Unsecured resources must be configured in OAuth2Config’s configure method otherwise they will be treated as secured resources. AuthServer resources should be Unsecured resources as they will help to retrieve access token.

@RestController
public class AuthServer {

    TokenService tokenService;

    @Autowired
    public AuthServer(TokenService tokenService) {
        this.tokenService = tokenService;
    }

    @GetMapping("/auth_server/config")
    public AuthServerResponse configurations(){
       return tokenService.getServerDetails();
    }


    @PostMapping("/access_token")
    public AuthenticationResult authorizeToken(@RequestBody @Valid AuthorizationRequest authorizationCode) throws Exception {
        return tokenService.getAccessTokenFromAuthorizationCode(authorizationCode.getCode(), authorizationCode.getRedirectUri());
    }

    @PostMapping("/refresh/access_token")
    public AuthenticationResult refreshToken(@RequestBody @Valid AuthorizationRequest authorizationCode) throws Exception {
        return tokenService.getAccessTokenFromRefreshToken(authorizationCode.getRefreshToken(), authorizationCode.getRedirectUri());
    }

}

AuthServerResponse is response to api “/auth_server/config” providing client-id, tenant and URL that client will use to get authorization code.

public class AuthServerResponse {
    private String clientId;
    private String tenantId;
    private String authority;

    public AuthServerResponse(String clientId, String tenantId, String authority) {
        this.clientId = clientId;
        this.tenantId = tenantId;
        this.authority = authority;
    }

    public String getClientId() {
        return clientId;
    }

    public void setClientId(String clientId) {
        this.clientId = clientId;
    }

    public String getTenantId() {
        return tenantId;
    }

    public void setTenantId(String tenantId) {
        this.tenantId = tenantId;
    }

    public String getAuthority() {
        return authority;
    }

    public void setAuthority(String authority) {
        this.authority = authority;
    }
}

Token Service

public interface TokenService {

    AuthenticationResult getAccessTokenFromAuthorizationCode(String authorizationCode, String redirectUri) throws Exception;

    AuthenticationResult getAccessTokenFromRefreshToken(String refreshToken, String redirectUri);

    AuthServerResponse getServerDetails();

}

Default Token Service

@Service
public class DefaultTokenService implements TokenService {

    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultTokenService.class);

    private TokenGenerator tokenGenerator;

    @Autowired
    DefaultTokenService(TokenGenerator tokenGenerator) {
        this.tokenGenerator = tokenGenerator;
    }

    @Override
    public AuthenticationResult getAccessTokenFromAuthorizationCode(String authorizationCode, String redirectUri) throws Exception {
        AuthorizationCode request = new AuthorizationCode(authorizationCode);
        try {
            return tokenGenerator.getAccessToken(request, redirectUri);
        } catch (Throwable throwable) {
            return throwException(throwable);
        }
    }

    @Override
    public AuthenticationResult getAccessTokenFromRefreshToken(String refreshToken, String redirectUri) {
        try {
            return tokenGenerator.getAccessTokenFromRefreshToken(refreshToken, redirectUri);
        } catch (Throwable throwable) {
            return throwException(throwable);
        }
    }

    @Override
    public AuthServerResponse getServerDetails() {
        return tokenGenerator.getServerDetails();
    }

    private AuthenticationResult throwException(Throwable throwable) {
        LOGGER.error(String.format("Failed To retrieve access token using refresh token"), throwable.getMessage());
        throw new TokenGenerationException("Users Access Could not be retrieved from Authentication Server");
    }
}
@Component
public class TokenGenerator {

    @Value("${security.oauth2.client.clientId}")
    private String clientId;

    @Value("${security.oauth2.client.clientSecret}")
    private String clientSecret;

    @Value("${security.oauth2.client.tenant}")
    private String tenant;

    @Value("${security.oauth2.client.authority}")
    private String authority;

    @Value("${security.oauth2.client.resource}")
    private String resource;

    public AuthenticationResult getAccessToken(
            AuthorizationCode authorizationCode, String currentUri)
            throws Throwable {
        String authCode = authorizationCode.getValue();
        ClientCredential credential = new ClientCredential(clientId,
                clientSecret);
        AuthenticationContext context = null;
        AuthenticationResult result = null;
        ExecutorService service = null;
        try {
            service = Executors.newFixedThreadPool(1);
            context = new AuthenticationContext(authority + tenant + "/", true,
                    service);
            Future<AuthenticationResult> future = context
                    .acquireTokenByAuthorizationCode(authCode, new URI(
                            currentUri), credential, resource, null);
            result = future.get();
        } catch (ExecutionException e) {
            throw e.getCause();
        } finally {
            service.shutdown();
        }

        if (result == null) {
            throw new ServiceUnavailableException(
                    "authentication result was null");
        }
        return result;
    }

    private AuthenticationResult getAccessTokenFromClientCredentials()
            throws Throwable {
        AuthenticationContext context = null;
        AuthenticationResult result = null;
        ExecutorService service = null;
        try {
            service = Executors.newFixedThreadPool(1);
            context = new AuthenticationContext(authority + tenant + "/", true,
                    service);
            Future<AuthenticationResult> future = context.acquireToken(
                    "https://graph.windows.net", new ClientCredential(clientId,
                            clientSecret), null);
            result = future.get();
        } catch (ExecutionException e) {
            throw e.getCause();
        } finally {
            service.shutdown();
        }

        if (result == null) {
            throw new ServiceUnavailableException(
                    "authentication result was null");
        }
        return result;
    }

    public AuthenticationResult getAccessTokenFromRefreshToken(
            String refreshToken, String currentUri) throws Throwable {
        AuthenticationContext context = null;
        AuthenticationResult result = null;
        ExecutorService service = null;
        try {
            service = Executors.newFixedThreadPool(1);
            context = new AuthenticationContext(authority + tenant + "/", true,
                    service);
            Future<AuthenticationResult> future = context
                    .acquireTokenByRefreshToken(refreshToken,
                            new ClientCredential(clientId, clientSecret), null,
                            null);
            result = future.get();
        } catch (ExecutionException e) {
            throw e.getCause();
        } finally {
            service.shutdown();
        }

        if (result == null) {
            throw new ServiceUnavailableException(
                    "authentication result was null");
        }
        return result;

    }


    public AuthServerResponse getServerDetails() {
        return new AuthServerResponse(clientId, tenant, authority);
    }


}

 

public class AuthorizationRequest {
    String code;
    String redirectUri;
    String refreshToken;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getRedirectUri() {
        return redirectUri;
    }

    public void setRedirectUri(String redirectUri) {
        this.redirectUri = redirectUri;
    }

    public String getRefreshToken() {
        return refreshToken;
    }

    public void setRefreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
    }
}
@ResponseStatus(HttpStatus.FAILED_DEPENDENCY)
public class TokenGenerationException extends RuntimeException {

    public TokenGenerationException(String message) {
        super(message);
    }
}

@ResponseStatus(HttpStatus.FORBIDDEN)
public class UserAccessFailedException extends RuntimeException {

    public UserAccessFailedException(String message){
        super(message);
    }
}

Secured Resources: are those which must have a valid access token that are authorized by authorization server

We have user resources which will be secured and protected from un authorized access in application. Such as user email will be retrieved if user is authenticated.

@RestController
@RequestMapping("/user")
public class UserResource {

    @GetMapping("/email")
    public String email(){
        OAuth2Authentication authentication = (OAuth2Authentication)SecurityContextHolder.getContext().getAuthentication();;
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                (UsernamePasswordAuthenticationToken) authentication.getUserAuthentication();
        return (String)((Map) usernamePasswordAuthenticationToken.getDetails())
                .getOrDefault("userPrincipalName","Result Not Found");
    }

}

And Finally, a main class to boot strap our application as Spring Boot application.

@SpringBootApplication
public class AzureOauth2Application {

   public static void main(String[] args) {
      SpringApplication.run(AzureOauth2Application.class, args);
   }
}

Now running above code if user resource is accessed without providing access token it will generate Unauthorized error as follows with status 401.

To Obtain Access Token we shall use Authorization Code Grant Flow:

First of all invoke GET URL http://localhost:8080/auth_server/config

Which will generate following response

{
      “clientId”: “06a8ddb3-79b1-4826-a61c”,
      “tenantId”: “ceddc98e-d9e8-4242-bd71”,
      “authority”: “https://login.microsoftonline.com/” 
}

From above response create a URL which will look like as follows:

https://login.microsoftonline.com/ceddc98e-d9e8-4242-bd71/oauth2/authorize?client_id=06a8ddb3-79b1-4826-a61c&response_type=code&redirect_uri=http://localhost:8080/

Note : Make sure redirect_uri must be registered as Reply URL in Azure Active Directory.

After you hit above URL it will redirect request on redirect_url with a code query parameter, after user is successfully authenticated on azure authorization server.

This code is very short-lived token and is required in subsequent request to azure server.

 Get access token using authorization code:

Invoke API http://localhost:8080/access_token to generate access token like as follows :

Response to above api hit look like:

{
      "accessTokenType":"Bearer",
      "expiresOn":3600,
      "idToken":" IsInVuaXF1ZV9uYW1lIjoiY2t1bWFyLnhlYmlhQEZMWVNQSUNFLkNPTSIsInVwbiI6ImNrdW1hci54ZWJpYUBGTFlTUElDRS5DT00iLCJ2ZXIiOiIxLjAifQ.",
      "userInfo":{
      "uniqueId":"907789eb-fdbf-4ea9-87a3-d0642df899c5",
      "displayableId":"ckumar.xebia@xebia.com",
      "givenName":"chandan",
      "familyName":"Kumar",
      "identityProvider":null,
      "passwordChangeUrl":null,
      "passwordExpiresOn":null
      },
      "accessToken":" aI6u72PhZzyA5cFya7XT43CNLfkdFCsliDQu_Q4pRS59LHYZafZRSRrIpfRanex-OYLVl-sEu7rQhFRAUk56BKlKbjzkLx7olmQ6yL2hxq4jSA",
      "refreshToken":" PHbJnNvhSNQjdLpJnso3lNy_JcaU4m1UACPmAAhlzdpXjYXtQV66vH1vPu2KZNLYxrkysVseENMTAaI4tHmDUPBnZVJ829HqPJQ91SGG5XYB_IAA",
      "multipleResourceRefreshToken":true,
      "expiresOnDate":1513259365702,
      "expiresAfter":3600
}

Now using access_token obtained in previous request we can hit our secured resources with Authorization bearer header as follows and it will allow result where earlier it was saying Unauthorized Access.

Get access token using refresh token:

Now once user is already authenticated and if access token is expired using refresh token access token can be again retrieved via following api.

Response of this API would be same as of access_token api. now new access_token obtained can be used to further request secured resources same way as user/email api is accessed.

This is how your application may use Azure Active Directory users to Sign in your app without keeping local user database and their credentials.

Source code is available here

 

Leave a Reply

Your email address will not be published. Required fields are marked *