Search code examples
javaspring-bootspring-securityazure-active-directoryazure-ad-b2b

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


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?


Solution

  • @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.