Using Azure AD premium custom roles with spring security for role based access

Viewed 3433

I have created a demo spring boot application where i want to use AD authentication and authorization using AD and spring security.Looking at Azure docs i did the following

package com.myapp.contactdb.contactfinder;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

@RequestMapping("/directory")
public interface Directory {
    @Autowired
    @PreAuthorize("hasRole('Users')")
    @GetMapping("/contact/{mobile}")
    public String getContact(@PathVariable("mobile") Long mobile);
    
    @Autowired
    
    @GetMapping("/contact/data")
    public String getData();

}

which is the rest API entry point. I created groups and users in it in the respective Azure AD.And used that group as specified in azure docs like this

package com.myapp.contactdb;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.microsoft.azure.spring.autoconfigure.aad.AADAppRoleStatelessAuthenticationFilter;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
     
    
    @Autowired
    private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .oauth2Login()
            .userInfoEndpoint()
            .oidcUserService(oidcUserService);
    }
    
}

and app properties as

spring.main.banner-mode=off

# create and drop tables and sequences, loads import.sql
#spring.jpa.hibernate.ddl-auto=create-drop

# MySql settings
spring.datasource.url=jdbc:mysql://localhost:3306/xxxx
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL8Dialect

# HikariCP settings
# spring.datasource.hikari.*

spring.datasource.hikari.connection-timeout=60000
spring.datasource.hikari.maximum-pool-size=5

# azure.activedirectory.tenant-id
azure.activedirectory.tenant-id = xxxx
azure.activedirectory.client-id = xxxx


# spring.security.oauth2.client.registration.azure.client-id
spring.security.oauth2.client.registration.azure.client-id = xxxxxxx

# spring.security.oauth2.client.registration.azure.client-secret
spring.security.oauth2.client.registration.azure.client-secret = xxxxxxxx

azure.activedirectory.active-directory-groups =  Users

However i require to authorize using custom roles.I have added an azure premium AD free trial and created a role viz., "Operator". However problem is what property do i use to depict that in the app.props file and how to get the role to get reflected in the @Preauthorize(hasRole('Operator')). Any idea or anything that i may have not seen?

2 Answers

@Jim,

So finally i went with this by modifying the WebSecurityConfig class from above in the question

package com.xxx.contactdb;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;

import net.minidev.json.JSONArray;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated().and().oauth2Login().userInfoEndpoint()
                .oidcUserService(this.oidcUserService());
    }

    /**
     * Replaces the granted authorities value received in token with the roles value
     * in token received from the app roles attribute defined in manifest and
     * creates a new OIDCUser with updated mappedAuthorities
     * 
     * @return oidcUser
     */
    private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
        final OidcUserService delegate = new OidcUserService();

        return (userRequest) -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
            // Delegate to the default implementation for loading a user
            OidcUser oidcUser = delegate.loadUser(userRequest);
            oidcUser.getAuthorities().forEach(authority -> {
                if (OidcUserAuthority.class.isInstance(authority)) {
                    OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;
                    Map<String, Object> userInfo = oidcUserAuthority.getAttributes();
                    JSONArray roles = null;
                    if (userInfo.containsKey("roles")) {
                        try {
                            roles = (JSONArray) userInfo.get("roles");
                            roles.forEach(s -> {
                                mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + (String) s));
                            });
                        } catch (Exception e) {
                            // Replace this with logger during implementation
                            e.printStackTrace();
                        }
                    }
                }
            });
            oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());

            return oidcUser;
        };
    }

}

I did this change for spring boot version 2.3.1 Release which uses Azure 2.3.1 and spring security version 5.3.3. Mentoned this because for Spring boot version 2.1.13 we could use UserAuthoritiesMapping as the authorities would have a OIDCUserService type mapping which the latest one doesnt. However if one uses DB to populate the roles to the Granted Authorities then they can still go with this option and not the OidcUser option.This is working as of now.

If you want to project your application with app role, please refer to the following steps

The example is used app role auth to project web api

  1. Register web API application and configure API scope

  2. Define app role in your Azure AD web api application. Please add the following content in your application manifest

  "appRoles": [
    {
      "allowedMemberTypes": [
        "User"
      ],
      "displayName": "Admin",
      "id": "2fa848d0-8054-4e11-8c73-7af5f1171001",
      "isEnabled": true,
      "description": "Full admin access",
      "value": "Admin"
    },
    {
      "allowedMemberTypes": [
        "User"
      ],
      "displayName": "User",
      "id": "f8ed78b5-fabc-488e-968b-baa48a570001",
      "isEnabled": true,
      "description": "Normal user access",
      "value": "User"
    }
  ],
  1. Assign these roles for the user

  2. Register client application in Azure AD and configure API permissions

  3. Enable implicit flow in client application

  4. Configure API application

a. sdk

  <dependency>
            <groupId>com.microsoft.azure</groupId>
            <artifactId>azure-active-directory-spring-boot-starter</artifactId>
           <version>2.3.1</version>
        </dependency>

b. application.properties

azure.activedirectory.session-stateless=true
azure.activedirectory.client-id=xxxxxx-your-client-id-xxxxxx
azure.activedirectory.appIdUri=xxxxxx-your-appIDUri-xxxxxx

c. WebSecurityConfig class

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AADAppRoleStatelessAuthenticationFilter aadAuthFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);

        http.authorizeRequests()
            .antMatchers("/", "/index.html", "/public").permitAll()
            .anyRequest().authenticated();

        http.addFilterBefore(aadAuthFilter, UsernamePasswordAuthenticationFilter.class);

    }
}

d. Controller

@RestController
public class HelloController {

    @GetMapping("/public")
    @ResponseBody
    public String publicMethod() {
        return "public endpoint response";
    }

    @GetMapping("/authorized")
    @ResponseBody
    @PreAuthorize("hasAnyRole('User','Admin')")
    public String onlyAuthorizedUsers() {

        return "authorized endpoint response";
    }

    @GetMapping("/admin/demo")
    @PreAuthorize("hasRole('Admin')")
    @ResponseBody
    public String onlyForAdmins() {
        return "admin endpoint";
    }
}
  1. Test. I use a single page to do a test

enter image description here enter image description here

For more details, please refer to here and here

Related