Spring Security, Powered by MSAL Part 3 (Client Auth and Resource Server)

Spring Security, Powered by MSAL Part 3 (Client Auth and Resource Server)

In the Part 1 and Part 2 of this series, we discussed two common Auth use cases; Client Auth Mode and the Resource Server Mode of integrating MSAL with Spring Boot. In this article, we will look at another common use case; the Client Auth and Resource Server mode.

Use Case

Let’s say you have an application with and API endpoint you would like to expose to another application in your organization. And also in the same application, you want to allow users from your Azure Active Directory to have access to some other routes in the application.

App Registrations Setup

We would follow the App Registrations Setup described in the Second article in this Series.

Code Implementation

Dependencies

<properties>
    <java.version>11</java.version>
    <azure.version>3.11.0</azure.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.azure.spring</groupId>
        <artifactId>azure-spring-boot-starter-active-directory</artifactId>
        <version>${azure.version}</version>
    </dependency>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>31.0.1-jre</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

application.yml

azure:
  activedirectory:
    application-type: *web_application_and_resource_server
    *app-id-uri: ${APP_ID_URI}
    tenant-id: ${AZURE_TENANT_ID}
    client-id: ${AZURE_CLIENT_ID}
    client-secret: ${AZURE_CLIENT_SECRET}
    authorization-clients:
      azure:
        authorization-grant-type: authorization_code
        scopes: https://graph.microsoft.com/User.Read

SecurityAdapter

package com.example.demosecurity;

import com.azure.spring.aad.webapi.AADResourceServerWebSecurityConfigurerAdapter;
import com.azure.spring.aad.webapp.AADWebSecurityConfigurerAdapter;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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.web.csrf.CookieCsrfTokenRepository;


@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2Config {

    private static final String *APPROLE_ROLE_1 *= "APPROLE_ROLE_1";
    private static final String *APPROLE_ROLE_2 *= "APPROLE_ROLE_2";

    @Order(1)
    @Configuration
    public static class ApiWebSecurityConfigurationAdapter extends AADResourceServerWebSecurityConfigurerAdapter {
        protected void configure(HttpSecurity http) throws Exception {
            super.configure(http);
            http.antMatcher("/api/**").authorizeRequests().anyRequest().hasAnyAuthority(*APPROLE_ROLE_1*, *APPROLE_ROLE_2*).and().oauth2Login();
        }
    }

    @Configuration
    public static class HtmlWebSecurityConfigurerAdapter extends AADWebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            super.configure(http);
            http
                    .csrf()
                    .csrfTokenRepository(CookieCsrfTokenRepository.*withHttpOnlyFalse*())
                    .requireCsrfProtectionMatcher(httpServletRequest -> false)
                    .and()
                    .authorizeRequests()
                    .antMatchers("/actuator/health", "/public/*").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .oauth2Login();
        }
    }

}

Controller

@RestController
public class DemoController {

    @GetMapping(value = "/demo")
    public String demo() {
        return "demo";
    }

    @GetMapping(value = "/api/demo")
    public String apiDemo() {
        return "demo";
    }
}

Conclusion

With this setup, the regular user can view all the routes except the routes starting with /api, but the Api consumer can only access routes starting with /api.

The source code for this article can be found on Github.