Manual authentication, custom authorization w/ OAuth & hierarchical authorities configuration (Spring Boot)

Introduction

Parent page "Spring Security (Spring Boot)":

https://sites.google.com/site/pawneecity/sprint-boot/spring-security-spring-boot



This sample shows a Spring Boot 2.6.0 application that receives the token of an already authenticated user in a 3rd party authentication system.

The application enforces manual authentication and custom authorization based on the authorities received from another 3rd party authorization system.

Furthermore, the application configures an authority hierarchy for easy handling of the authorities returned by the 3rd party authorization system.

References

Spring Security > Authorization > Expression-Based Access Control

(current) https://docs.spring.io/spring-security/reference/servlet/authorization/expression-based.html

(5.5.x) https://docs.spring.io/spring-security/site/docs/5.5.x/reference/html5/#el-access

Intro to Spring Security Expressions

https://www.baeldung.com/spring-security-expressions

How to Manually Authenticate User with Spring Security

https://www.baeldung.com/manually-set-user-authentication-spring-security

Spring Security without the WebSecurityConfigurerAdapter (Spring Boot >= 2.7)

https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

Spring Security - How to Fix WebSecurityConfigurerAdapter Deprecated (Spring Boot >= 2.7)

https://www.codejava.net/frameworks/spring-boot/fix-websecurityconfigureradapter-deprecated

POM configuration

pom.xml

  <dependency>

   <groupId>org.springframework.boot</groupId>

   <artifactId>spring-boot-starter-security</artifactId>

  </dependency>


Security configuration

MethodSecurityConfig (3.3 <= Spring Boot)

package sample.autho2;


import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;

import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;

import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;


/**

 * Configuration for setting a role hierarchy.

 *

 * <pre>

 * Following is needed for PrePost work in controllers:

 * (at)Configuration

 * (at)EnableMethodSecurity(prePostEnabled = true)

 * </pre>

 */

@Configuration

@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)

public class MethodSecurityConfig {


  @Bean

  RoleHierarchy roleHierarchy() {

    return RoleHierarchyImpl.fromHierarchy( //

        "ROLE_SUPER > ROLE_LIBRARIAN_ADMIN > ROLE_LIBRARIAN\n" //

            + "ROLE_SUPER > ROLE_SHOEMAKER\n" //

            + "ROLE_SUPER > ROLE_WRITER\n" //

            + "ROLE_SUPER > CREATE_REPORT\n" //

            + "ROLE_SUPER > EXTRACT_DATA\n" //

            + "ROLE_SUPER > MONITOR_APP\n" //

            + "ROLE_SUPER > IMPERSONATE_USER" //

    );

  }


  /** Since Spring Boot 3.x (Spring Security 6.0), this bean replaces the previous RoleHierarchyVoter one. */

  @Bean

  DefaultMethodSecurityExpressionHandler expressionHandler() {

    DefaultMethodSecurityExpressionHandler expressionHandler

      = new DefaultMethodSecurityExpressionHandler();

    expressionHandler.setRoleHierarchy(roleHierarchy());


    return expressionHandler;

  }


}


MethodSecurityConfig (3.0 <= Spring Boot < 3.3)

package sample.autho2;


import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;

import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;

import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;


/**

 * Configuration for setting a role hierarchy.

 *

 * <pre>

 * Following is needed for PrePost work in controllers:

 * (at)Configuration

 * (at)EnableMethodSecurity(prePostEnabled = true)

 * </pre>

 */

@Configuration

@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)

public class MethodSecurityConfig {


  @Bean

  RoleHierarchy roleHierarchy() {

    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();

    roleHierarchy.setHierarchy(//

        "ROLE_SUPER > ROLE_LIBRARIAN_ADMIN > ROLE_LIBRARIAN\n" //

            + "ROLE_SUPER > ROLE_SHOEMAKER\n" //

            + "ROLE_SUPER > ROLE_WRITER\n" //

            + "ROLE_SUPER > CREATE_REPORT\n" //

            + "ROLE_SUPER > EXTRACT_DATA\n" //

            + "ROLE_SUPER > MONITOR_APP\n" //

            + "ROLE_SUPER > IMPERSONATE_USER" //

    );


    return roleHierarchy;

  }


  /** Since Spring Boot 3.x (Spring Security 6.0), this bean replaces the previous RoleHierarchyVoter one. */

  @Bean

  DefaultMethodSecurityExpressionHandler expressionHandler() {

    DefaultMethodSecurityExpressionHandler expressionHandler

      = new DefaultMethodSecurityExpressionHandler();

    expressionHandler.setRoleHierarchy(roleHierarchy());


    return expressionHandler;

  }


}


GlobalMethodSecurityConfig.java (Spring Boot < 3.0)

package sample.autho2;


import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;

import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;

import org.springframework.security.access.hierarchicalroles.RoleHierarchy;

import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;

import org.springframework.security.access.vote.RoleHierarchyVoter;

import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;


/**

 * 

 * Configuration for setting a role hierarchy.<br>

 * 

 * Following is needed for PrePost work in controllers:<br>

 * (at)EnableGlobalMethodSecurity(prePostEnabled = true)

 *

 */

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)

public class GlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {


  /*- DOC. Since Spring Boot 2.6; it's detected as a circular reference.

  @Autowired

  private RoleHierarchy roleHierarchy

   */


  /*- DOC. At Spring Boot 2.6; it's done by:

   * GlobalMethodSecurityConfiguration.afterSingletonsInstantiated()

  @Override

  protected MethodSecurityExpressionHandler createExpressionHandler() [

    return methodSecurityExpressionHandler()

  ]

  */


  /*- DOC. At Spring Boot 2.6; it's done by:

   * GlobalMethodSecurityConfiguration.afterSingletonsInstantiated()

  private DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() [

    var expressionHandler = new DefaultMethodSecurityExpressionHandler()

    expressionHandler.setRoleHierarchy(this.roleHierarchy)

    return expressionHandler

  ]

  */


  @Bean

  public RoleHierarchy roleHierarchy() {

    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();

    roleHierarchy.setHierarchy(//

        "ROLE_SUPER > ROLE_LIBRARIAN_ADMIN > ROLE_LIBRARIAN\n" //

            + "ROLE_SUPER > ROLE_SHOEMAKER\n" //

            + "ROLE_SUPER > ROLE_WRITER\n" //

            + "ROLE_SUPER > CREATE_REPORT\n" //

            + "ROLE_SUPER > EXTRACT_DATA\n" //

            + "ROLE_SUPER > MONITOR_APP\n" //

            + "ROLE_SUPER > IMPERSONATE_USER" //

    );


    return roleHierarchy;

  }


  @Bean

  public RoleHierarchyVoter roleVoter() {

    return new RoleHierarchyVoter(roleHierarchy());

  }


}



WebSecurityConfig.java (3.2 <= Spring Boot)

Eg: defense

package sample.autho32;


import org.springframework.beans.factory.annotation.Value;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.core.annotation.Order;

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.WebSecurityCustomizer;

import org.springframework.security.config.http.SessionCreationPolicy;

import org.springframework.security.web.SecurityFilterChain;

import org.springframework.security.web.firewall.StrictHttpFirewall;


import sample.autho32.security.oauth.OauthAuthenEntryPoint;

import sample.autho32.security.oauth.OauthAuthenProvider;

import sample.autho32.security.oauth.OauthSecurityConfigurer;

import jakarta.inject.Inject;

import lombok.extern.slf4j.Slf4j;


@Slf4j

@Configuration

@EnableWebSecurity

public class WebSecurityConfig {


  /** Data REST base path */

  private String basePathDataRest;


  /*- */

  private OauthAuthenProvider oauthTokenProvider;


  /**

   * Constructor.

   * 

   * @param basePathDataRest   -

   * @param oauthTokenProvider -

   */

  @Inject

  public WebSecurityConfig(

      @Value("${spring.data.rest.base-path:'/rest/merida'}") String basePathDataRest, //

      OauthAuthenProvider oauthTokenProvider//

  ) {

    super();

    this.basePathDataRest = basePathDataRest;

    this.oauthTokenProvider = oauthTokenProvider;

  }


  /**

   * Configuration for allowing special characters, e.g: ';', '\', '/' and '%'.

   * 

   * @return -

   */

  @Bean

  WebSecurityCustomizer webSecurityCustomizer() {

    var firewall = new StrictHttpFirewall();


    // symbol '/' encoded as '%2F' [didn't work w/ SpringBoot/3.1.3 that uses Tomcat/10.1.12]

    firewall.setAllowUrlEncodedSlash(true);


    /*- symbol '\'

    firewall.setAllowBackSlash(true)

    */


    /*- symbol ';'

    firewall.setAllowSemicolon(true)

    */


    /*- symbol '%' encoded as '%25'

    firewall.setAllowUrlEncodedPercent(true)

    */


    return web -> web.httpFirewall(firewall);

  }


  /**

   * @param http

   * @return -

   * @throws Exception

   */

  @SuppressWarnings({ "java:S1612" })

  @Order(1)

  @Bean

  SecurityFilterChain filterChain1(HttpSecurity http) throws Exception {

    http//

        .securityMatcher(basePathDataRest, basePathDataRest + "/explorer/**") // Apply only if match

        .cors(withDefaults())// exclude OPTIONS requests from authorization checks

        // .and()//

        .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()) //

        .csrf(csrf -> csrf.disable()) //

        .headers(headers -> headers.frameOptions(fo -> fo.disable()))//

        // .and()//

        .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//

    ;


    if (log.isDebugEnabled()) {

      log.debug("END WebSecurityConfig.filterChain1");

    }


    return http.build();

  }


  /**

   * @param http

   * @return -

   * @throws Exception

   */

  @SuppressWarnings({ "java:S1612" }) // Unhandled token in '@SuppressWarnings': Ingore

  @Order(2)

  @Bean

  SecurityFilterChain filterChain2(HttpSecurity http) throws Exception {

    http// sb3

        .securityMatcher(basePathDataRest + "/**")//

        .authorizeHttpRequests(authorize -> authorize.anyRequest().hasAuthority("SUPER"))// NOSONAR

        // .apply(new OauthSecurityConfigurer(oauthTokenProvider))

        .with(new OauthSecurityConfigurer(oauthTokenProvider), withDefaults());

    /*

     * 'apply' For removal in 7.0. Use

     * AbstractConfiguredSecurityBuilder.with(SecurityConfigurerAdapter, Customizer) instead.

     */


    http.exceptionHandling(handling -> handling.authenticationEntryPoint(

        new OauthAuthenEntryPoint()))//

        .csrf(csrf -> csrf.disable())//

        .headers(headers -> headers.frameOptions(fo -> fo.disable()))//

        .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//

    ;


    if (log.isDebugEnabled()) {

      log.debug("END WebSecurityConfig.filterChain2");

    }


    return http.build();

  }


  /**

   * Actuator.<br>

   * http://localhost:8080/rest/actuator<br>

   * <br>

   * 

   * @param http

   * @return -

   * @throws Exception

   */

  @SuppressWarnings({ "java:S1612" })

  @Order(3)

  @Bean

  SecurityFilterChain filterChain3(HttpSecurity http) throws Exception {

    http// sb3

        .securityMatcher("/rest/actuator", "/rest/actuator/**") //

        .authorizeHttpRequests(authorize -> authorize.anyRequest().hasAuthority("SUPER"))// NOSONAR

        // .apply(new OauthSecurityConfigurer(oauthTokenProvider))

        .with(new OauthSecurityConfigurer(oauthTokenProvider), withDefaults());

    /*

     * 'apply' For removal in 7.0. Use

     * AbstractConfiguredSecurityBuilder.with(SecurityConfigurerAdapter, Customizer) instead.

     */


    http.exceptionHandling(handling -> handling.authenticationEntryPoint(

        new OauthAuthenEntryPoint()))//

        .csrf(csrf -> csrf.disable())//

        .headers(headers -> headers.frameOptions(fo -> fo.disable()))//

        .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//

    ;


    if (log.isDebugEnabled()) {

      log.debug("END WebSecurityConfig.filterChain3");

    }


    return http.build();

  }


  /**

   * Paths from coming from the APP API Gateway which do not require any additional security

   * check:<br>

   * -"/rest/export/**" > OAuth token already verified by API Gateway<br>

   * -"/rest/monitor/**" > OAuth token already verified by API Gateway<br>

   * -"/rest/public/**" > Public path<br>

   * 

   * @param http

   * @return -

   * @throws Exception

   */

  @SuppressWarnings({ "java:S1612" })

  @Order(4)

  @Bean

  SecurityFilterChain filterChain4(HttpSecurity http) throws Exception {

    if (log.isDebugEnabled()) {

      log.debug("INI WebSecurityConfig.filterChain4");

    }


    http// sb3

        .securityMatcher("/rest/export/**", "/rest/monitor/**", "/rest/public/**") //

        .cors(withDefaults())//

        .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()) //

        .csrf(csrf -> csrf.disable())//

        .headers(headers -> headers.frameOptions(fo -> fo.disable()))//

        .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//

    ;


    if (log.isDebugEnabled()) {

      log.debug("END WebSecurityConfig.filterChain4");

    }


    return http.build();

  }


  /**

   * Security entry point for paths with OAuth token that require affiliations plus autho

   * tables.<br>

   * Note: It has order 5, therefore it will only execute if no adapter with order<5 matches.

   * 

   * @param http

   * @return -

   * @throws Exception

   */

  @SuppressWarnings({ "java:S1612" })

  @Order(5)

  @Bean

  SecurityFilterChain filterChain5Oauth(HttpSecurity http) throws Exception {

    if (log.isDebugEnabled()) {

      log.debug("INI WebSecurityConfig.filterChain5Oauth");

    }


    http// sb3

        .securityMatcher("/rest/**")//

        .cors(withDefaults())//

        .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())// NOSONAR

        // .apply(new OauthSecurityConfigurer(this.oauthTokenProvider))

        .with(new OauthSecurityConfigurer(this.oauthTokenProvider), withDefaults());

    /*

     * 'apply' For removal in 7.0. Use

     * AbstractConfiguredSecurityBuilder.with(SecurityConfigurerAdapter, Customizer) instead.

     */


    http.exceptionHandling(handling -> handling.authenticationEntryPoint(

        new OauthAuthenEntryPoint()))//

        .csrf(csrf -> csrf.disable())//

        .headers(headers -> headers.frameOptions(fo -> fo.disable()))//

        .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//

    ;

    if (log.isDebugEnabled()) {

      log.debug("END WebSecurityConfig.filterChain5Oauth");

    }


    return http.build();

  }


  /**

   * Springdoc v2 (for Spring Boot >= 3.0).

   * 

   * @param http

   * @return -

   * @throws Exception

   */

  @SuppressWarnings({ "java:S1612", "java:S6212" })

  @Order(6)

  @Bean

  SecurityFilterChain filterChainSpringDocV2(HttpSecurity http) throws Exception {

    final String[] pathAnonymous = // NOSONAR (SONARJAVA-3991)

        { "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**" };


    http// sb3

        .securityMatcher(pathAnonymous) // only invoke if matching

        .authorizeHttpRequests(authorize -> authorize//

            .anyRequest().anonymous()//

        ) //

    ;


    http.csrf(csrf -> csrf.disable())//

        .headers(headers -> headers.frameOptions(fo -> fo.disable()));//

    // DOC. Avoid generation of cookie JSESSIONID (Swagger)

    http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));


    return http.build();

  }


  /**

   * Paths which aren't exposed in the App API Gateway and therefore only usable directly:<br>

   * 

   * <pre>

   * /h2-console/**

   * /sub2/** (subprocess 2) *** !!!

   * </pre>

   * 

   * @param http

   * @return -

   * @throws Exception

   */

  @SuppressWarnings({ "java:S1612" })

  @Order(7)

  @Bean

  SecurityFilterChain filterChainNoApiGtw(HttpSecurity http) throws Exception {

    http// sb3

        .securityMatcher("/h2-console/**", "/sub2/**") //

        .cors(withDefaults()) //

        .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()) //

        .csrf(csrf -> csrf.disable())//

        .headers(headers -> headers.frameOptions(fo -> fo.disable()))//

        .sessionManagement(management -> management.sessionCreationPolicy(

            SessionCreationPolicy.STATELESS))//

    ;


    if (log.isDebugEnabled()) {

      log.debug("END WebSecurityConfig.filterChainNoApiGtw");

    }


    return http.build();

  }


  /**

   * Other anonymous.

   * 

   * @param http

   * @return -

   * @throws Exception

   */

  @SuppressWarnings({ "java:S1612", "java:S6212" })

  @Order(7)

  @Bean

  SecurityFilterChain filterChainOtherAnonymous(HttpSecurity http) throws Exception {

    final String[] pathAnonymous = { "/ui/**", "/monitor/**", "/tech/**", "/favicon*", "/version",

        "/csrf" };


    // , "/webjars/**", "/swagger-resources/**", "/", "/v2/api-docs/**"


    http// sb3

        .securityMatcher(pathAnonymous) // Apply only if match

        .authorizeHttpRequests(authorize -> authorize//

            .anyRequest().anonymous()//

        ) //

    ;


    /*- sb3

    http // NOSONAR

        .authorizeRequests().antMatchers(pathAnonymous).permitAll() // NOSONAR

    */


    http.csrf(csrf -> csrf.disable())//

        .headers(headers -> headers.frameOptions(fo -> fo.disable()));

    http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));


    return http.build();

  }


}


WebSecurityConfig.java (Spring Boot < 3.2)

package sample.autho2;


import org.springframework.context.annotation.Configuration;

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;


/**

 * Data REST HAL Explorer didn't work fine on AWS because links are generated with private load

 * balancer address, instead of the public CloudFront one; although Data REST works fine.

 */

@Configuration

@EnableWebSecurity

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


  /** Data REST base path */

  public static final String BASE_PATH_DATA_REST = "/rest/merida";


  /**

   * Spring Boot >= 3.0 equivalence of the configure method

  @Order(6)

  @Bean

  SecurityFilterChain filterChain6(HttpSecurity http) throws Exception {

    http//

        .authorizeHttpRequests().requestMatchers("/", "/home", "/*", "/h2-console/**", BASE_PATH_DATA_REST + "/explorer/**").permitAll()//

        .cors()// exclude OPTIONS requests from authorization checks

        .and()//

        .csrf().disable().headers().frameOptions().disable()//

        .and()//

        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//

    ;


    return http.build();

  }

   */


  /**

   * Spring Boot >= 2.7 <3.0 equivalence of the configure method

  @Order(6)

  @Bean

  SecurityFilterChain filterChain6(HttpSecurity http) throws Exception {

    http.requestMatchers().antMatchers("/", "/home", "/*", "/h2-console/**", BASE_PATH_DATA_REST + "/explorer/**") //

        .and()//

        .cors()// exclude OPTIONS requests from authorization checks

        .and() //

        .authorizeRequests().anyRequest().permitAll() //

        .and()//

        .csrf().disable().headers().frameOptions().disable()//

        .and()//

        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//

    ;


    return http.build();

  }

   */


  /** configure method for Spring Boot < 2.7 */

  @Override

  protected void configure(HttpSecurity http) throws Exception {

    http // NOSONAR

        .authorizeRequests() // NOSONAR

        .antMatchers("/", "/home", "/*").permitAll() // NOSONAR

        .antMatchers("/h2-console/**").permitAll() // NOSONAR

        .antMatchers(BASE_PATH_DATA_REST + "/explorer/**").permitAll() // NOSONAR

    ;


    http.csrf().disable();

    http.headers().frameOptions().disable();

    // DOC. Avoid generation of cookie JSESSIONID (Swagger)

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

  }


}



Manual authentication

Since the user is already authenticated in a 3rd party system, we'll manually authenticate the user in Spring Boot so that the framework can enforce authorization with it.

The idea is to intercept all the http requests to the REST controller, and register in the security context the authentication based on the http header X-Api-Key of the request that holds the token provided by the 3rd party authentication system.


CustomInvalidAuthenExceptionTest.java

package sample.autho2.exception;


import org.springframework.http.HttpStatus;

import org.springframework.lang.Nullable;

import org.springframework.security.core.AuthenticationException;


/**

 * Exception related to an Authentication object being invalid for whatever reason.

 * 

 * Note: This class extends the Spring Boot AuthenticationException (not a custom one).

 *

 * 401 'Unauthenticated' http status code.

 *

 * Note that, semantically, http defines two different codes:<br>

 * ·401 for 'unauthenticated' (user does not have a valid session)<br>

 * ·403 for 'unauthorized' (user has a valid session, but he's not authorized)<br>

 */

public class CustomInvalidAuthenException extends AuthenticationException {


  private static final long serialVersionUID = -371528602897747414L;


  /** REST response suggested http status code. */

  public static final HttpStatus HTTP_STATUS = HttpStatus.UNAUTHORIZED;


  /**

   * Constructor.

   * 

   * @param message -

   */

  public CustomInvalidAuthenException(final String message) {

    this(message, null);

  }


  /**

   * Constructor

   * 

   * @param message .

   * @param cause   Nullable

   */

  public CustomInvalidAuthenException(final String message, final @Nullable Throwable cause) {

    super(message, cause);

  }


}


CustomInvalidAuthenExceptionTest.java

package sample.autho2.exception;


import static org.junit.jupiter.api.Assertions.assertEquals;

import static org.junit.jupiter.api.Assertions.assertNotNull;


import org.junit.jupiter.api.Test;

import org.springframework.http.HttpStatus;


import lombok.val;


/**

 * .

 */

class CustomInvalidAuthenExceptionTest {


  /***/

  @Test

  void allTest() {

    assertEquals(HttpStatus.UNAUTHORIZED, CustomInvalidAuthenException.HTTP_STATUS);


    val i = new CustomInvalidAuthenException("I'm a unit test");

    assertNotNull(i);

  }


}


CustomAuthenInterceptor.java is in charge of intercepting all http requests and of registering the 'Authentication' instance in the security context

package sample.autho2.facade;


import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Scope;

import org.springframework.context.annotation.ScopedProxyMode;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.context.SecurityContext;

import org.springframework.security.core.context.SecurityContextHolder;

import org.springframework.web.context.WebApplicationContext;

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;


/**

 * User authentication interceptor.<br>

 * <br>

 * 

 * (at)ComponentScan needed to be able to inject CustomAuthenProvider.<br>

 * (at)Configuration unused because Bean created by CustomAuthenConfig<br>

 */

@ComponentScan

public class CustomAuthenInterceptor extends HandlerInterceptorAdapter {


  private static final Logger log = LoggerFactory.getLogger(CustomAuthenInterceptor.class);


  protected static final String HEADER_SESSION = "Authorization";


  private LocaleResolver localeResolver;

  private CustomAuthenProvider authenProvider;


  /**

   * @param localeResolver

   * @param userDetailsService

   */

  @Autowired

  public CustomAuthenInterceptor(final @NotNull LocaleResolver localeResolver,

      final @NotNull UserDetailsService userDetailsService) {

    super();

    this.localeResolver = localeResolver;

    this.authenProvider = new CustomAuthenProvider(userDetailsService);

  }


  @Override

  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)

      throws Exception {


    log.info("Request intercepted, triggering manual authentication");


    final String headerAuthorization = request.getHeader(HEADER_SESSION);


    final String oauthToken;

    if (headerAuthorization == null || !headerAuthorization.startsWith(

        SecurityHelper.AUTHEN_BEARER_PREFIX)) {

      oauthToken = null;

    } else {

      oauthToken = headerAuthorization.substring(SecurityHelper.AUTHEN_BEARER_PREFIX.length());

    }


    final Locale l = this.localeResolver.resolveLocale(request);


    this.triggerManualAuthentication(oauthToken, l.getLanguage());


    return true;

  }


  /**

   * Register the 'authentication' in the security context.

   * 

   * @param tokenOauth OAuth token

   * @param l          Language ISO 639-1 code.

   */

  @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)

  public void triggerManualAuthentication(final String tokenOauth, final @NotNull String l) {

    log.info("INI Manual authentication...");


    Authentication authenRequest = new CustomPreAuthenticatedSession(tokenOauth);

    Authentication authenResponse = this.authenProvider.authenticate(authenRequest);


    // Set user language

    if (authenResponse.getPrincipal() != null) {

      ((CustomUserDetails) authenResponse.getPrincipal()).setLangIso639p1Code(l);

    }


    //

    SecurityContext sc = SecurityContextHolder.getContext();

    sc.setAuthentication(authenResponse);


    log.info("END Manual authentication.\nisAuthenticated=" + authenResponse.isAuthenticated()

        + "\nAuthorities=" + authenResponse.getAuthorities() + "\nPrincipal=" + authenResponse

            .getPrincipal());

  }


}



CustomAuthenProvider.java is in charge of obtaining the Authorization instance to be registered in the security context

package sample.autho2.facade;


import org.springframework.security.authentication.AuthenticationProvider;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.AuthenticationException;

//import org.springframework.stereotype.Component;


/*-@Component*/

public class CustomAuthenProvider implements AuthenticationProvider {


  /*-

   * @return a fully authenticated object including credentials. May return

   * <code>null</code> if the <code>AuthenticationProvider</code> is unable to support

   * authentication of the passed <code>Authentication</code> object. In such a case,

   * the next <code>AuthenticationProvider</code> that supports the presented

   * <code>Authentication</code> class will be tried.

   * @throws AuthenticationException if authentication fails.

   */

  @Override

  @Nullable

  public Authentication authenticate(Authentication authenRequest) throws AuthenticationException {

    Authentication authenResponse;


    var /* Object */ oauthTokenObject = authenRequest.getCredentials();

    String oauthToken;

    try {

      oauthToken = (String) oauthTokenObject;

    }catch(ClassCastException cce) {

      // This <code>AuthenticationProvider</code> is unable to support authentication of the passed <code>Authentication</code> object

      return null;

    }

    

    

    // DOC Use the credentials for retrieving user details from 3rd party system

    CustomUserDetails principalNullable = this.ubOauthAuthorizerService.getUserDetails(oauthToken);

    if (null == principalNullable) {

      // Provided 'oauthToken' is not valid

      //throw new CustomInvalidAuthenException("Provided 'oauthToken' is not valid")

      authenResponse = authenRequest;

    } else {

      authenResponse = new OauthPreAuthenticatedSession(principalNullable, oauthToken);

    }


    return authenResponse;

  }



  @Override

  public boolean supports(Class<?> authentication) {

    return authentication.equals(CustomPreAuthenticatedSession.class);

  }

}


CustomAuthenConfig.java is in charge of registering the request interceptor for all requests to path "/user/**"

package sample.autho2.facade;


import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


/**

 * Configure request interceptor for path "/rest/**".

 */

@Configuration

public class CustomAuthenConfig implements WebMvcConfigurer {


  private LocaleResolver localeResolver;

  private UserDetailsService userDetailsService;


  /**

   * Constructor.

   * 

   * @param localeResolver     -

   * 

   * @param userDetailsService -

   */

  @Autowired

  public CustomAuthenConfig(final @NotNull LocaleResolver localeResolver,

      final @NotNull UserDetailsService userDetailsService) {

    super();

    this.localeResolver = localeResolver;

    this.userDetailsService = userDetailsService;

  }


  /**

   * If you're using Spring Data REST in your project, and you want to intercept your Spring

   * Repositories and @RepositoryRestControllers, you need to define this @Bean method.<br>

   * <br>

   * This method must be declared in any @Configuration annotated class; and such class does not

   * need to implement/extend anything special. The interceptor implementation must either implement

   * HandlerInterceptor or extend HandlerInterceptorAdapter.<br>

   * 

   * @return -

   */

  @Bean

  public MappedInterceptor customAuthenInterceptor() {

    //[a] Secure APIs

    String[] includePatterns = { "/rest/**" };

    //[b] Allow public

    String[] excludePatterns = { "/rest/public/**" };


    return new MappedInterceptor(includePatterns, excludePatterns, new CustomAuthenInterceptor(

        this.localeResolver, this.userDetailsService));

  }


  @Override

  public void addInterceptors(InterceptorRegistry registry) {

    registry.addInterceptor(this.customAuthenInterceptor());

  }


  /**

   * CORS values ​​that our application has

   */

  @Override

  public void addCorsMappings(CorsRegistry registry) {

    registry.addMapping("/**").allowedMethods("GET", "POST", "DELETE", "PATCH", "PUT", "OPTIONS")

        .allowedHeaders("*");

  }


}


 

CustomPreAuthenticatedSession.java is a custom implementation for the pre-authenticated session id (generated by the 3rd party authentication system)


package sample.autho2.facade;


import org.springframework.security.authentication.AbstractAuthenticationToken;

import org.springframework.security.core.SpringSecurityCoreVersion;


/**

 * {@link org.springframework.security.core.Authentication} implementation for custom

 * pre-authenticated session identifier.

 * <br>

 * From that javadoc: "Callers are expected to populate the principal for an authentication

 * request."

 */

public class CustomPreAuthenticatedSession extends AbstractAuthenticationToken {


  private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;


  private final CustomUserDetails principal;


  /**

   * Credentials (the sessionId)

   */

  private final String credentials;


  /**

   * Constructor used for an authentication request. The

   * {@link org.springframework.security.core.Authentication#isAuthenticated()} will return

   * <code>false</code>.

   *

   * @param aPrincipal The pre-authenticated principal

   * @param sessionId  The pre-authenticated session id

   */

  public CustomPreAuthenticatedSession(String sessionId) {

    super(null);

    this.principal = null;

    this.credentials = sessionId;

  }


  /**

   * Constructor used for an authentication response. The

   * {@link org.springframework.security.core.Authentication#isAuthenticated()} will return

   * <code>true</code>.

   *

   * @param aPrincipal The authenticated principal, with its authorities

   */

  public CustomPreAuthenticatedSession(CustomUserDetails aPrincipal, String sessionId) {

    super(aPrincipal.getAuthorities());

    this.principal = aPrincipal;

    this.credentials = sessionId;

    setAuthenticated(true);

  }


  /**

   * Getter sessionId.

   */

  @Override

  public Object getCredentials() {

    return this.credentials;

  }


  /*-

   * The identity of the principal being authenticated. Callers are

   * expected to populate the principal for an authentication request.

   * <p>

   * The <tt>AuthenticationManager</tt> implementation will often return an

   * <tt>Authentication</tt> containing richer information as the principal for use by

   * the application. Many of the authentication providers will create a

   * {@code UserDetails} object as the principal.

   *

   * @return the <code>Principal</code> being authenticated or the authenticated

   * principal after authentication.

   */

  @Override

  public CustomUserDetails getPrincipal() {

    return this.principal;

  }


}



CustomUserDetails.java extends the framework class UserDetails, adding the application specific user info returned by 3rd party system based on the session id.

package sample.autho2.facade;


import java.io.Serializable;

import java.util.Collection;

import java.util.Collections;

import java.util.Comparator;

import java.util.Set;

import java.util.SortedSet;

import java.util.TreeSet;


import javax.validation.constraints.NotNull;


import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.SpringSecurityCoreVersion;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.util.Assert;


/**

 * Custom user details retrieved from 3rd party system.<br>

 * <br>

 * Example of response from 3rd party system:<br>

 * 

 * <pre>

         <out>

            <extendedSessionCampus>

               <appId>INST</appId>

               <appIdTREN>INST</appIdTREN>

               <iso639Language>en</iso639Language>

               <languageId>c</languageId>

               <loginTime>2020-01-12T11:24:38+01:00</loginTime>

               <userDni>36241169T</userDni>

               <userName>First Name</userName>

               <userNumber>272876</userNumber>

               <userSex>M</userSex>

               <userSubTypeId>inst</userSubTypeId>

               <userSurname1>Lastname1</userSurname1>

               <userSurname2>Lastname2</userSurname2>

               <userTypeId>CONS</userTypeId>

            </extendedSessionCampus>

            <idp>272876</idp>

            <sessionId>598612657</sessionId>

            <userLogin>username</userLogin>

         </out>

 * </pre>

 */

public class CustomUserDetails implements UserDetails {

  

  /**

   * 

   */

  private static final long serialVersionUID = 4206778189244514942L;

  /**

   * 3rd party system attribute 'userLogin'.<br>

   */

  private final String username;


  /**

   * 3rd party system authorities, retrieved via operation getUserRoles(idp, module).<br>

   * Note that a role is just a type of authority that begins with "ROLE_".<br>

   * 

   * NonNull. Lazy loading, do always use getter!

   */

  private final Set<GrantedAuthority> authorities;


  /**

   * 3rd party system returns userName + " " + userSurname1 + " " + userSurname2.

   *

   */

  @NotNull

  private String displayName; // NOSONAR


  /**

   * Idp of the user.<br>

   * Note: Since release 2.4, it's used by front-end because of inc. vgpra-294

   */

  @NotNull

  private Long idp = Long.valueOf(0); // NOSONAR


  /**

   * Language code, eg: 'ca', 'en' or 'es' (retrieved from 3rd party system).

   */

  @NotNull

  private String langIso639p1Code; // NOSONAR


  /**

   * Constructor.

   * 

   * @param username

   * @param password

   * @param enabled

   * @param accountNonExpired

   * @param credentialsNonExpired

   * @param accountNonLocked

   * @param authorities

   */

  public CustomUserDetails(String username, Collection<? extends GrantedAuthority> authorities,

      String displayName, long idp, String langIso639p1Code) {


    if (((username == null) || "".equals(username)) || (displayName == null)) {

      throw new IllegalArgumentException("Cannot pass null or empty values to constructor");

    }


    // DOC. Attributes needed because of extending UserDetails

    this.username = username;

    this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));


    // DOC. Custom attributes

    this.displayName = displayName;

    this.idp = Long.valueOf(idp);

    this.langIso639p1Code = langIso639p1Code;


  }


  /**

   * Cannot return <code>null</code>.

   */

  @Override

  public String getUsername() {

    return this.username;

  }


  @Override

  public Collection<? extends GrantedAuthority> getAuthorities() {

    return this.authorities;

  }


  @Override

  public String getPassword() {

    return null;

  }


  @Override

  public boolean isAccountNonExpired() {

    return true;

  }


  @Override

  public boolean isAccountNonLocked() {

    return true;

  }


  @Override

  public boolean isCredentialsNonExpired() {

    return true;

  }


  @Override

  public boolean isEnabled() {

    return true;

  }

  

  

  @Override

  public String toString() {

    return "["+username+", "+displayName+", "+idp+", "+langIso639p1Code+"]";

  }


  private static SortedSet<GrantedAuthority> sortAuthorities(

      Collection<? extends GrantedAuthority> authorities) {

    Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");

    // Ensure array iteration order is predictable (as per

    // UserDetails.getAuthorities() contract and SEC-717)

    SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet<>(new AuthorityComparator());


    for (GrantedAuthority grantedAuthority : authorities) {

      Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements");

      sortedAuthorities.add(grantedAuthority);

    }


    return sortedAuthorities;

  }


  private static class AuthorityComparator implements Comparator<GrantedAuthority>, Serializable {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;


    public int compare(GrantedAuthority g1, GrantedAuthority g2) {

      // Neither should ever be null as each entry is checked before adding it to

      // the set.

      // If the authority is null, it is a custom authority and should precede

      // others.

      if (g2.getAuthority() == null) {

        return -1;

      }


      if (g1.getAuthority() == null) {

        return 1;

      }


      return g1.getAuthority().compareTo(g2.getAuthority());

    }

  }


}



ThirdPartyAuthorizerSimulator.java simulates 3rd party system responses with user details and authorities (for session ids 's0' and 's27)


package sample.autho2.facade;


import java.util.ArrayList;

import java.util.Collection;


import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.authority.SimpleGrantedAuthority;


/**

 * It simulates responses from a 3rd party Authorization system.<br>

 * <br>

 * It uses 3 hardcoded sessionIds for testing purposes:<br>

 * <br>

 * 

 * <pre>

 * sessionId description

 * s0        Granted authority ROLE_SUPER (see role hierarchy in GlobalMethodSecurityConfig)

 * s27       Granted authorities (eg: ROLE_LIBRARIAN, MONITOR_APP)

 * [other]   Non-valid sessionId

 * </pre>

 */

public class ThirdPartyAuthorizerSimulator {


  /**

   * 

   * @param sessionId (only "s27" is considered valid)

   * @return User details if sessionId is valid, null otherwise

   */

  public static CustomUserDetails getUserDetails(String sessionId) {

    if ("s0".equals(sessionId)) {


      long idp0 = 0L;


      return new CustomUserDetails("root", ThirdPartyAuthorizerSimulator.getUserAuthorities(idp0),

          "Name zero", idp0, "en");


    } else if ("s27".equals(sessionId)) {

      String username = "user27";

      String displayName = "Name 27";

      long idp = 27;

      String langCode = "pl";


      CustomUserDetails ret = new CustomUserDetails(username, ThirdPartyAuthorizerSimulator

          .getUserAuthorities(idp), displayName, idp, langCode);

      return ret;

    } else {

      return null;

    }


  }


  /**

   * Remark: There is a role hierarchy configured in:<br>

   * GlobalMethodSecurityConfig

   * 

   * @param idp

   * @return

   */

  private static Collection<? extends GrantedAuthority> getUserAuthorities(long idp) {

    Collection<SimpleGrantedAuthority> ret = new ArrayList<SimpleGrantedAuthority>();


    if (0 == idp) {

      // Simulate user with ROLE_SUPER

      ret.add(new SimpleGrantedAuthority("ROLE_SUPER"));

      // (by hierarchy)ret.add(new SimpleGrantedAuthority("MONITOR_APP"));

      // (by hierarchy)ret.add(new SimpleGrantedAuthority("IMPERSONATE_USER"));

    } else if (27 == idp) {

      // DOC. Adding roles


      ret.add(new SimpleGrantedAuthority("ROLE_SHOEMAKER"));

      ret.add(new SimpleGrantedAuthority("ROLE_LIBRARIAN"));

      ret.add(new SimpleGrantedAuthority("ROLE_WRITER"));

      ret.add(new SimpleGrantedAuthority("ROLE_LIBRARIAN_ADMIN"));


      // DOC. Adding other permissions

      ret.add(new SimpleGrantedAuthority("CREATE_REPORT"));

      ret.add(new SimpleGrantedAuthority("EXTRACT_DATA"));

    }


    return ret;

  }


}




Sample REST controller

This controller shows how to enforce authorization with @PreAuthorize and expressions like hasRole, hasAuthority or principal.

Note that because of he authority hierarchical configuration, a user with role ROLE_SUPER will be able to execute any method without expliciting it in the expression (it can be retrieved with the operation '/user/authorities').

package sample.autho2.facade;


import static org.springframework.web.bind.annotation.RequestMethod.GET;


import java.util.Collection;

import java.util.stream.Collectors;


import javax.validation.constraints.NotNull;


import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.access.hierarchicalroles.RoleHierarchy;

import org.springframework.security.access.prepost.PreAuthorize;

import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.context.SecurityContextHolder;

import org.springframework.web.bind.annotation.RequestHeader;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.bind.annotation.RestController;


/**

 * See ThirdPartyAuthorizerSimulator for mapping between sessionId (s27) and authorities

 * (ROLE_LIBRARIAN, MONITORAPP).

 *

 */

@RequestMapping(path = "/user")

@RestController

public class GeneralUserController {


  /**

   * If need to manually retrieve ReachableGrantedAuthorities

   */

  private RoleHierarchy roleHierarchy;


  /**

   * Constructor.

   */

  @Autowired

  public GeneralUserController(@NotNull RoleHierarchy roleHierarchy) {

    this.roleHierarchy = roleHierarchy;

  }


  /**

   * Find out all reacheable granted authorities of the user.<br>

   * <br>

   * (a) sessionId 's27'<br>

   * curl http://localhost:8080/user/authorities -H "X-Api-Key: s27" <br>

   * <br>

   * (b) sessionId 's0'<br>

   * curl http://localhost:8080/user/authorities -H "X-Api-Key: s0"

   */

  @PreAuthorize("permitAll()")

  @RequestMapping(value = "/authorities", method = GET)

  @ResponseBody

  public String getAuthorities(@RequestHeader(name = "X-Api-Key",

      required = true) @NotNull String sessionId // NOSONAR

  ) {

    System.out.println("INI getAuthorities");

    @SuppressWarnings("unchecked")

    Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) SecurityContextHolder

        .getContext().getAuthentication().getAuthorities();


    Collection<? extends GrantedAuthority> hierarchyAuthorities = roleHierarchy

        .getReachableGrantedAuthorities(authorities);


    // System.out.println("Authorities: " + hierarchyAuthorities);


    Collection<GrantedAuthority> roles = hierarchyAuthorities.stream().filter(sgu -> sgu

        .getAuthority().startsWith("ROLE_")).collect(Collectors.toList());


    // System.out.println("Roles: " + roles);

    return "\nAuthorities: " + hierarchyAuthorities + "\nRoles: " + roles + "\n";

  }


  /**

   * (a) s27 has role ROLE_LIBRARIAN<br>

   * curl http://localhost:8080/user/role/librarian -H "X-Api-Key: s27" <br>

   * (b) sessionId 's0' has ROLE_SUPER, which indirectly contains ROLE_LIBRARIAN<br>

   * curl http://localhost:8080/user/role/librarian -H "X-Api-Key: s0"

   */

  @PreAuthorize("hasRole('ROLE_LIBRARIAN')")

  @RequestMapping(value = "/role/librarian", method = GET)

  @ResponseBody

  public String getRoleLibrarian(@RequestHeader(name = "X-Api-Key",

      required = true) @NotNull String sessionId // NOSONAR

  ) {

    System.out.println("INI getRoleLibrarian");

    return "ok\n";

  }


  /**

   * Session s27 does not have ROLE_SUPER<br>

   * curl http://localhost:8080/user/role/super -H "X-Api-Key: s27" <br>

   */

  @PreAuthorize("hasRole('ROLE_SUPER')")

  @RequestMapping(value = "/role/super", method = GET)

  @ResponseBody

  public String getRoleSuper(@RequestHeader(name = "X-Api-Key",

      required = true) @NotNull String sessionId // NOSONAR

  ) {

    return "KO\n";

  }


  /**

   * s027 has role ROLE_LIBRARIAN<br>

   * curl http://localhost:8080/user/role/librarianWithoutPrefix -H "X-Api-Key: s27"

   */

  @PreAuthorize("hasRole('LIBRARIAN')")

  @RequestMapping(value = "/role/librarianWithoutPrefix", method = GET)

  @ResponseBody

  public String getRoleLibrarianWithoutPrefixn(@RequestHeader(name = "X-Api-Key",

      required = true) @NotNull String sessionId // NOSONAR

  ) {

    return "ok\n";

  }


  /**

   * 

   * s027 does NOT have role MONITOR_APP (the authority is not a role)<br>

   * curl http://localhost:8080/user/role/monitor -H "X-Api-Key: s27"

   */

  @PreAuthorize("hasRole('MONITOR_APP')")

  @RequestMapping(value = "/role/monitor", method = GET)

  @ResponseBody

  public String getRoleMonitor(@RequestHeader(name = "X-Api-Key",

      required = true) @NotNull String sessionId // NOSONAR

  ) {

    return "KO\n";

  }


  /**

   * 

   * s027 has role ROLE_SHOEMAKER<br>

   * curl http://localhost:8080/user/role/anyYes -H "X-Api-Key: s27"

   */

  @PreAuthorize("hasAnyRole('NON_EXISTENT','SHOEMAKER','MONITOR_APP')")

  @RequestMapping(value = "/role/anyYes", method = GET)

  @ResponseBody

  public String getRoleAnyYes(@RequestHeader(name = "X-Api-Key",

      required = true) @NotNull String sessionId // NOSONAR

  ) {

    return "ok\n";

  }


  /**

   * 

   * s027 does not have any of specified roles<br>

   * curl http://localhost:8080/user/role/anyNo -H "X-Api-Key: s27"

   */

  @PreAuthorize("hasAnyRole('NON_EXISTENT','ROLE_NON_EXISTENT','CREATE_REPORT')")

  @RequestMapping(value = "/role/anyNo", method = GET)

  @ResponseBody

  public String getRoleAnyNo(@RequestHeader(name = "X-Api-Key",

      required = true) @NotNull String sessionId // NOSONAR

  ) {

    return "KO\n";

  }


  /**

   * 

   * s027 has authority MONITOR_APP<br>

   * curl http://localhost:8080/user/authority/monitor -H "X-Api-Key: s27"

   */

  @PreAuthorize("hasAuthority('MONITOR_APP')")

  @RequestMapping(value = "/authority/monitor", method = GET)

  @ResponseBody

  public String getAuthorityMonitor(@RequestHeader(name = "X-Api-Key",

      required = true) @NotNull String sessionId // NOSONAR

  ) {

    return "ok";

  }


  /**

   * 

   * s027 has authority ROLE_LIBRARIAN<br>

   * curl http://localhost:8080/user/authority/librarian -H "X-Api-Key: s27"

   */

  @PreAuthorize("hasAuthority('ROLE_LIBRARIAN')")

  @RequestMapping(value = "/authority/librarian", method = GET)

  @ResponseBody

  public String getAuthorityLibrarian(@RequestHeader(name = "X-Api-Key",

      required = true) @NotNull String sessionId // NOSONAR

  ) {

    return "ok\n";

  }


  /**

   * 

   * s027 does not have authority NON_EXISTENT<br>

   * curl http://localhost:8080/user/authority/nonExistent -H "X-Api-Key: s27"

   */

  @PreAuthorize("hasAuthority('NON_EXISTENT')")

  @RequestMapping(value = "/authority/nonExistent", method = GET)

  @ResponseBody

  public String getAuthorityNonexistent(@RequestHeader(name = "X-Api-Key",

      required = true) @NotNull String sessionId // NOSONAR

  ) {

    return "KO\n";

  }


  /**

   * 

   * s027 has at least one authority<br>

   * curl http://localhost:8080/user/authority/anyYes -H "X-Api-Key: s27"

   */

  @PreAuthorize("hasAnyAuthority('NON_EXISTENT','ROLE_WRITER','MONITOR_APP')")

  @RequestMapping(value = "/authority/anyYes", method = GET)

  @ResponseBody

  public String getAuthorityAnyYes(@RequestHeader(name = "X-Api-Key",

      required = true) @NotNull String sessionId // NOSONAR

  ) {

    return "ok";

  }


  /**

   * 

   * s027 does not have any authority<br>

   * curl http://localhost:8080/user/authority/anyNo -H "X-Api-Key: s27"

   */

  @PreAuthorize("hasAnyAuthority('LIBRARIAN','ROLE_NON_EXISTENT','ROLE_MONITOR_APP')")

  @RequestMapping(value = "/authority/anyNo", method = GET)

  @ResponseBody

  public String getAuthorityAnyNo(@RequestHeader(name = "X-Api-Key",

      required = true) @NotNull String sessionId // NOSONAR

  ) {

    return "KO\n";

  }


  /**

   * Authorize ony if username is 'user27'.<br>

   * <br>

   * 

   * sessionId s27 has the intended username<br>

   * curl http://localhost:8080/user/username -H "X-Api-Key: s27"

   */

  @PreAuthorize("principal.username == 'user27'")

  @RequestMapping(value = "/username", method = GET)

  @ResponseBody

  public String getUsername(@RequestHeader(name = "X-Api-Key",

      required = true) @NotNull String sessionId // NOSONAR

  ) {

    return "ok\n";

  }


}