Manual authentication, custom authorization w/ JWT & 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 implements a custom authorization in the following way:

    -It offers an operation for exchanging an OAuth token by a JWT token, afterwards all other operations perform authorization based on that JWT token.

    -It also demonstrates working with hierarchical authorities. Users are granted explicit roles, but further authorities are reachable by Spring Security hierarchical resolution.


In short:


References

https://docs.spring.io/spring-security/site/docs/current/reference/html/authorization.html

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

https://auth0.com/blog/implementing-jwt-authentication-on-spring-boot/


Sample usage

Users and their roles are 'simulated' by class ThirdPartyAuthorizerSimulator.


- [A] 't0' is a valid OAuth token for the user 'root'

Let's first exchange the OAuth token by the JWT token:

curl http://localhost:8080/rest/authen/token -H "Authorization: Bearer t0"


{"userDetails":{"username":"root","roles":["ROLE_GESTOR","ROLE_ADMIN","ROLE_MEMBRE"],"idp":0,"langIso639p1Code":"en"},"token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyb290IiwiYXV0aG9yaXRpZXMiOlsiU1VQRVIiXSwiZGlzcGxheU5hbWUiOiJOYW1lIHplcm8iLCJpZHAiOjAsImxhbmdJc282MzlwMUNvZGUiOiJlbiIsImlhdCI6MTU4MTc2MzQxNSwiZXhwIjoxNTgxODQ5ODE1fQ.iEdCFsi1lU7kqiuNUcl6HwWjllqAbUqVlr5AJFzzKRE"}


Let's then invoke business operations using the JWT token:

curl http://localhost:8080/rest/sample/username -H "X-Api-Key: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyb290IiwiYXV0aG9yaXRpZXMiOlsiU1VQRVIiXSwiZGlzcGxheU5hbWUiOiJOYW1lIHplcm8iLCJpZHAiOjAsImxhbmdJc282MzlwMUNvZGUiOiJlbiIsImlhdCI6MTU4MTc2MzQxNSwiZXhwIjoxNTgxODQ5ODE1fQ.iEdCFsi1lU7kqiuNUcl6HwWjllqAbUqVlr5AJFzzKRE"


- [B] 't27' is a valid OAuth token for the user 'user27'

Let's first exchange the OAuth token by the JWT token:

curl http://localhost:8080/rest/authen/token -H "Authorization: Bearer t27"


{"userDetails":{"username":"user27","roles":["ROLE_GESTOR","ROLE_MEMBRE"],"idp":27,"langIso639p1Code":"pl"},"token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMjciLCJhdXRob3JpdGllcyI6WyJST0xFX0dFU1RPUiJdLCJkaXNwbGF5TmFtZSI6Ik5hbWUgMjciLCJpZHAiOjI3LCJsYW5nSXNvNjM5cDFDb2RlIjoicGwiLCJpYXQiOjE1ODE3NjI4MDYsImV4cCI6MTU4MTg0OTIwNn0.HS5w0A0e_so5kqJEm0su-nqt4sau1W5WJVAOP8N1P6o"}


Let's then invoke business operations using the JWT token:

curl http://localhost:8080/rest/sample/role/librarian -H "X-Api-Key: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMjciLCJhdXRob3JpdGllcyI6WyJST0xFX0dFU1RPUiJdLCJkaXNwbGF5TmFtZSI6Ik5hbWUgMjciLCJpZHAiOjI3LCJsYW5nSXNvNjM5cDFDb2RlIjoicGwiLCJpYXQiOjE1ODE3NjI4MDYsImV4cCI6MTU4MTg0OTIwNn0.HS5w0A0e_so5kqJEm0su-nqt4sau1W5WJVAOP8N1P6o"


POM configuration (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"

  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

 <modelVersion>4.0.0</modelVersion>

 <parent>

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

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

  <version>2.2.4.RELEASE</version>

  <relativePath /> <!-- lookup parent from repository -->

 </parent>

 <groupId>sample</groupId>

 <artifactId>autho-custom-jwt</artifactId>

 <version>0.0.2-SNAPSHOT</version>

 <!--<name></name> -->

 <description>Authorization custom JWT</description>


 <properties>

  <java.version>11</java.version>

  <jjwt.version>0.11.0</jjwt.version>

 </properties>


 <dependencies>

  <dependency>

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

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

  </dependency>


  <dependency>

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

   <artifactId>spring-boot-starter-web</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-test</artifactId>

   <scope>test</scope>

   <exclusions>

    <exclusion>

     <groupId>org.junit.vintage</groupId>

     <artifactId>junit-vintage-engine</artifactId>

    </exclusion>

   </exclusions>

  </dependency>


  <dependency>

   <groupId>org.sonatype.sisu</groupId>

   <artifactId>sisu-inject-bean</artifactId>

   <version>2.3.0</version>

  </dependency>


        <dependency>

            <groupId>io.jsonwebtoken</groupId>

            <artifactId>jjwt-api</artifactId>

            <version>${jjwt.version}</version>

        </dependency>

        <dependency>

            <groupId>io.jsonwebtoken</groupId>

            <artifactId>jjwt-impl</artifactId>

            <version>${jjwt.version}</version>

            <scope>runtime</scope>

        </dependency>

        <dependency>

            <groupId>io.jsonwebtoken</groupId>

            <artifactId>jjwt-jackson</artifactId>

            <version>${jjwt.version}</version>

            <scope>runtime</scope>

        </dependency>


  <dependency>

   <groupId>org.projectlombok</groupId>

   <artifactId>lombok</artifactId>

   <optional>true</optional>

  </dependency>


  <dependency>

      <!-- Desired by annotation @ConfigurationProperties -->

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

   <artifactId>spring-boot-configuration-processor</artifactId>

   <optional>true</optional>

  </dependency>



 </dependencies>


 <build>

  <plugins>

   <plugin>

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

    <artifactId>spring-boot-maven-plugin</artifactId>

   </plugin>

  </plugins>

 </build>


</project>



Security configuration

MethodSecurityConfig (3.3 <= Spring Boot)

package sample.autho3;


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( //

        "SUPER > ROLE_ADMIN > ROLE_GESTOR > ROLE_MEMBRE\n" //

            + "ROLE_SUPER > MONITOR_APP" //

    );

  }


  /** 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.autho3;


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(//

        "SUPER > ROLE_ADMIN > ROLE_GESTOR > ROLE_MEMBRE\n" //

            + "ROLE_SUPER > MONITOR_APP" //

    );


    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;

  }


}


MethodSecurityConfig (Spring Boot < 3.0)

package sample.autho3;


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

  ]

  */


  /**

   * @return -

   */

  @Bean

  public RoleHierarchyImpl roleHierarchy() {

    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();

    roleHierarchy.setHierarchy(//

        "SUPER > ROLE_ADMIN > ROLE_GESTOR > ROLE_MEMBRE\n" //

            + "ROLE_SUPER > MONITOR_APP" //

    );


    return roleHierarchy;

  }


  /**

   * @return -

   */

  @Bean

  public RoleHierarchyVoter roleVoter() {

    return new RoleHierarchyVoter(roleHierarchy());

  }


}




Web security adapters

WebSecurityJwtConfigAdapter.java

package sample.autho3;


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

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

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


import sample.autho3.security.jwt.JwtAuthenProvider;

import sample.autho3.security.jwt.JwtSecurityConfigurer;


/**

 * Security entry point for paths with JWT token.<br>

 * SessionCreationPolicy.STATELESS mode prevents generation of cookie JSESSIONID

 */

@Configuration

@Order(1)

@EnableWebSecurity

public class WebSecurityJwtConfigAdapter extends WebSecurityConfigurerAdapter {


  /** Path of the operation for exchanging the OAuth token by the JWT token. */

  public static final String PATH_AUTHEN_TOKEN = "/rest/authen/token";


  /*- */

  private JwtAuthenProvider jwtTokenProvider;


  /**

   * Constructor.

   * 

   * @param jwtTokenProvider Injected

   */

  @Autowired

  public WebSecurityJwtConfigAdapter(JwtAuthenProvider jwtTokenProvider) {

    this.jwtTokenProvider = jwtTokenProvider;

  }


  @Override

  protected void configure(HttpSecurity http) throws Exception {

    if (log.isDebugEnabled()) {

      log.debug("INI WebSecurityJwtConfigAdapter.configure");

    }


    http.antMatcher("/r/j/**")//

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

        .and()//

        .authorizeRequests().anyRequest().authenticated()//

        .and()//

        .apply(new JwtSecurityConfigurer(jwtTokenProvider))//

        .and()//

        .exceptionHandling().authenticationEntryPoint(new JwtAuthenEntryPoint())//

        .and()//

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

        .and()//

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


    if (log.isDebugEnabled()) {

      log.debug("END WebSecurityJwtConfigAdapter.configure");

    }

  }


}


WebSecurityOauthConfigAdapter.java

package sample.autho3;


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

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

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


import sample.autho3.security.oauth.OauthAuthenProvider;

import sample.autho3.security.oauth.OauthSecurityConfigurer;


/**

 * Security entry point for paths with OAuth token.<br>

 * Note: It has order 2, therefore will only execute if no adapter with order 1 matches.<br>

 * <br>

 * 

 * SessionCreationPolicy.STATELESS mode prevents generation of cookie JSESSIONID

 */

@Configuration

@Order(2)

@EnableWebSecurity

public class WebSecurityOauthConfigAdapter extends WebSecurityConfigurerAdapter {



  /*- */

  private OauthAuthenProvider oauthTokenProvider;


  /**

   * Constructor.

   * @param oauthTokenProvider 

   * 

   */

  @Autowired

  public WebSecurityOauthConfigAdapter(OauthAuthenProvider oauthTokenProvider) {

    this.oauthTokenProvider = oauthTokenProvider;

  }


  @Override

  protected void configure(HttpSecurity http) throws Exception {

    if (log.isDebugEnabled()) {

      log.debug("INI WebSecurityOauthConfigAdapter.configure");

    }


    http.antMatcher("/r/w/authen/**")//

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

        .and()//

        .authorizeRequests().anyRequest().authenticated()//

        .and()//

        .apply(new OauthSecurityConfigurer(oauthTokenProvider))//

        .and()//

        .exceptionHandling().authenticationEntryPoint(new OauthAuthenEntryPoint())//

        .and()//

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

        .and()//

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


    if (log.isDebugEnabled()) {

      log.debug("END WebSecurityOauthConfigAdapter.configure");

    }

  }


}


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 the invocation to the operation that exchanges the Oauth token for the JWT one with a custom Authorizer thats expects the Oauth token at the header 'Authorization'.

All other invocations to other operations will be intercepted by another custom Authorizer that expects the JWT token at the header X-Api-Key.



CustomInvalidAuthenException.java

package sample.autho3.security;


import org.springframework.security.core.AuthenticationException;


/**

 *

 */

public class CustomInvalidAuthenException extends AuthenticationException {


  /**

   * 

   */

  private static final long serialVersionUID = -3022244538900203012L;


  /**

   * @param e

   */

  public CustomInvalidAuthenException(String e) {

    super(e);

  }


}



CustomUserDetails.java

package sample.autho3.security;


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;


import lombok.Getter;


/**

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

 * <br>

 */

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<? extends GrantedAuthority> authorities;


  /**

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

   */

  @Getter

  @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

   */

  @Getter

  @NotNull

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


  /**

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

   */

  @Getter

  @NotNull

  private String langIso639p1Code; // NOSONAR


  /**

   * Constructor.

   * 

   * @param username

   * @param password

   * @param enabled

   * @param accountNonExpired

   * @param credentialsNonExpired

   * @param accountNonLocked

   * @param authorities

   * @param displayName

   * @param idp

   * @param langIso639p1Code

   */

  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<? extends 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());

    }

  }


}



JwtAuthenEntryPoint.java

package sample.autho3.security.jwt;


import lombok.extern.slf4j.Slf4j;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.web.AuthenticationEntryPoint;


import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;


/**

 * JWT authentication entry point.

 *

 */

@Slf4j

public class JwtAuthenEntryPoint implements AuthenticationEntryPoint {


  @Override

  public void commence(HttpServletRequest request, HttpServletResponse response,

      AuthenticationException authException) throws IOException, ServletException {

    log.debug("JWT authentication failed:" + authException);

    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT authentication failed");

  }


}


 

JwtAuthenProvider.java


package sample.autho3.security.jwt;


import java.util.Collection;

import java.util.Date;

import java.util.HashSet;


import javax.annotation.PostConstruct;

import javax.crypto.SecretKey;

import javax.servlet.http.HttpServletRequest;


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

import org.springframework.security.authentication.AuthenticationProvider;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.core.GrantedAuthority;

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

import org.springframework.stereotype.Component;


import io.jsonwebtoken.Claims;

import io.jsonwebtoken.Jws;

import io.jsonwebtoken.JwtException;

import io.jsonwebtoken.Jwts;

import io.jsonwebtoken.SignatureAlgorithm;

import io.jsonwebtoken.security.Keys;

import sample.autho3.security.CustomInvalidAuthenException;

import sample.autho3.security.CustomUserDetails;


/**

 *

 */

@Component

public class JwtAuthenProvider implements AuthenticationProvider {


  @Autowired

  JwtProperties jwtProperties;


  private SecretKey secretKey;


  @PostConstruct

  protected void init() {

    secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);

  }


  /**

   * 

   * @param userDetails

   * @return -

   */

  public String createToken(final CustomUserDetails userDetails) {


    Claims claims = this.toClaims(userDetails);


    Date now = new Date();

    Date validity = new Date(now.getTime() + jwtProperties.getValidityInMs());


    return Jwts.builder()//

        .setClaims(claims)//

        .setIssuedAt(now)//

        .setExpiration(validity)//

        .signWith(secretKey)//

        .compact();

  }


  /**

   * Warning: toClaims() and toCustomUserDetails() must be aligned!

   * 

   * @param userDetails

   * @return JWT claims to be able to reconstruct the principal whenever the client sends the JWT

   *         token back

   */

  public Claims toClaims(final CustomUserDetails userDetails) {

    // (1)

    Claims claims = Jwts.claims().setSubject(userDetails.getUsername());

    // (2)authorities (as string, for avoiding deserialization issues)

    Collection<? extends GrantedAuthority> userAuthoritiesExplicit = userDetails.getAuthorities();

    Collection<String> uaes = new HashSet<>();

    for (GrantedAuthority j : userAuthoritiesExplicit) {

      uaes.add(j.getAuthority());

    }

    claims.put("authorities", uaes);

    // (3)

    claims.put("displayName", userDetails.getDisplayName());

    // (4)

    claims.put("idp", userDetails.getIdp());

    // (5)

    claims.put("langIso639p1Code", userDetails.getLangIso639p1Code());


    return claims;

  }


  /**

   * @param jwtToken

   * @return -

   */

  public CustomUserDetails toCustomUserDetails(final String jwtToken) {

    Jws<Claims> claims = this.getClaims(jwtToken);

    // (1)

    String username = this.getUsername(jwtToken);

    // (2)authorities (as string, for avoiding deserialization issues)

    @SuppressWarnings({ "unchecked" })

    Collection<String> uaes = (Collection<String>) claims.getBody().get("authorities");

    Collection<GrantedAuthority> authorities = new HashSet<>();

    for (String k : uaes) {

      authorities.add(new SimpleGrantedAuthority(k));

    }

    // (3)

    String displayName = (String) claims.getBody().get("displayName");

    // (4)

    Long idp = Long.parseLong(String.valueOf(claims.getBody().get("idp")));

    // (5)

    String langIso639p1Code = (String) claims.getBody().get("langIso639p1Code");


    //

    return new CustomUserDetails(username, authorities, displayName, idp, langIso639p1Code);

  }


  /**

   * @param token

   * @return -

   */

  @SuppressWarnings("deprecation")

  public String getUsername(String token) {

    return Jwts.parserBuilder().setSigningKey(signingKey).build().parseClaimsJws(token).getBody().getSubject();

  }


  /**

   * @param jwtToken

   * @return claims

   */

  public Jws<Claims> getClaims(String jwtToken) {

    /*- (old)

    Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);

    return claims;

     */

    return Jwts.parserBuilder().setSigningKey(signingKey).build().parseClaimsJws(jwtToken)

  }


  /**

   * @param req

   * @return -

   */

  public String resolveToken(HttpServletRequest req) {

    // DOC. X-Api-Key is an allowed header at the API Gateway

    String ret;


    String bearerToken = req.getHeader("X-Api-Key");

    if (bearerToken != null && bearerToken.startsWith("Bearer ")) {

      ret = bearerToken.substring(7, bearerToken.length());

    } else {

      ret = null;

    }

    return ret;

  }


  /**

   * @param jwtToken

   * @return -

   */

  public boolean validateToken(String jwtToken) {

    try {

      if (jwtToken == null) {

        return false;

      }


      @SuppressWarnings("deprecation")

      Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);


      if (claims.getBody().getExpiration().before(new Date())) {

        return false;

      }


      //

      return true;

    } catch (JwtException | IllegalArgumentException e) {

      log.warn("Expired or invalid JWT token", e);

      return false;

    }

  }


  @Override

  public Authentication authenticate(Authentication authenRequest) throws AuthenticationException {


    Authentication authenResponse;


    String jwtToken = (String) authenRequest.getCredentials();


    CustomUserDetails principal;

    if (validateToken(jwtToken)) {

      principal = this.toCustomUserDetails(jwtToken);

    } else {

      principal = null;

    }


    if (null == principal) {

      authenResponse = authenRequest;

    } else {

      authenResponse = new JwtPreAuthenticatedSession(principal, jwtToken);

    }


    return authenResponse;

  }


  @Override

  public boolean supports(Class<?> authentication) {

    return authentication.equals(JwtPreAuthenticatedSession.class);

  }


}





JwtPreAuthenticatedSession.java


package sample.autho3.security.jwt;


import org.springframework.security.authentication.AbstractAuthenticationToken;

import org.springframework.security.core.SpringSecurityCoreVersion;


import sample.autho3.security.CustomUserDetails;


/**

 * JWT implementation for pre-authenticated identifier.<br>

 * {@link org.springframework.security.core.Authentication} 

 * <br>

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

 * request."

 */

public class JwtPreAuthenticatedSession extends AbstractAuthenticationToken {


  private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;


  private final CustomUserDetails principal;


  /**

   * Credentials (the jwtToken)

   */

  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 jwtToken  The pre-authenticated JWT token

   */

  public JwtPreAuthenticatedSession(String jwtToken) {

    super(null);

    this.principal = null;

    this.credentials = jwtToken;

  }


  /**

   * 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 explicit authorities

   * @param jwtToken JWT token

   */

  public JwtPreAuthenticatedSession(CustomUserDetails aPrincipal, String jwtToken) {

    super(aPrincipal.getAuthorities());

    this.principal = aPrincipal;

    this.credentials = jwtToken;

    setAuthenticated(true);

  }


  /**

   * Getter OAuth token.

   */

  @Override

  public Object getCredentials() {

    return this.credentials;

  }


  /*- [pending: will need to be marked (at)NotNull when ready for it]

   * 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;

  }


}




JwtProperties.java

package sample.autho3.security.jwt;


import org.springframework.boot.context.properties.ConfigurationProperties;


import lombok.Data;

import org.springframework.context.annotation.Configuration;


/**

 * JWT secret key and validity.

 *

 */

@Configuration

@ConfigurationProperties(prefix = "jwt")

@Data

public class JwtProperties {


  // validity in milliseconds (24h. API gateway already checks oauth token at every request

  private long validityInMs = 24 * 60 * 60 * 1000;

}




JwtSecurityConfigurer.java

package sample.autho3.security.jwt;


import org.springframework.security.config.annotation.SecurityConfigurerAdapter;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.web.DefaultSecurityFilterChain;

import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


/**

 * It configures a filter for the JWT token manual authentication

 */

public class JwtSecurityConfigurer extends

    SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {


  private JwtAuthenProvider jwtTokenProvider;


  /**

   * @param jwtTokenProvider

   */

  public JwtSecurityConfigurer(JwtAuthenProvider jwtTokenProvider) {

    this.jwtTokenProvider = jwtTokenProvider;

  }


  @Override

  public void configure(HttpSecurity http) throws Exception {

    // Filter that processes the jwtToken if, and only if, it's not the authen/token operation


    JwtTokenAuthenFilter customFilter = new JwtTokenAuthenFilter(jwtTokenProvider);


    http.exceptionHandling().authenticationEntryPoint(new JwtAuthenEntryPoint()).and()

        .addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);

  }


}



JwtTokenAuthenFilter.java

package sample.autho3.security.jwt;


import java.io.IOException;


import javax.servlet.FilterChain;

import javax.servlet.ServletException;

import javax.servlet.ServletRequest;

import javax.servlet.ServletResponse;

import javax.servlet.http.HttpServletRequest;


import org.springframework.security.core.Authentication;

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

import org.springframework.web.filter.GenericFilterBean;


import lombok.extern.slf4j.Slf4j;


/**

 * Picks up the jwtToken (excluding the initial token request path) and registers the principal in

 * the security context.

 */

@Slf4j

public class JwtTokenAuthenFilter extends GenericFilterBean {


  private JwtAuthenProvider jwtAuthenProvider;


  // private String tokenRequestPath;


  /**

   * @param jwtTokenProvider

   */

  public JwtTokenAuthenFilter(JwtAuthenProvider jwtTokenProvider) {

    this.jwtAuthenProvider = jwtTokenProvider;

    // this.tokenRequestPath = tokenRequestPath;

  }


  @Override

  public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)

      throws IOException, ServletException {


    String jwtToken = jwtAuthenProvider.resolveToken((HttpServletRequest) req);


    triggerManualAuthentication(jwtToken);


    filterChain.doFilter(req, res);

  }


  /**

   * Register the 'authentication' in the security context.

   * 

   * @param jwtToken

   */

  public void triggerManualAuthentication(String jwtToken) {

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


    Authentication authenRequest = new JwtPreAuthenticatedSession(jwtToken);

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

    SecurityContextHolder.getContext().setAuthentication(authenResponse);


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

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

            .getPrincipal());


  }


}



OauthAuthenEntryPoint.java

package sample.autho3.security.oauth;


import lombok.extern.slf4j.Slf4j;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.web.AuthenticationEntryPoint;


import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;


/**

 * OAuth authentication entry point.

 *

 */

@Slf4j

public class OauthAuthenEntryPoint implements AuthenticationEntryPoint {


  @Override

  public void commence(HttpServletRequest request, HttpServletResponse response,

      AuthenticationException authException) throws IOException, ServletException {

    log.debug("OAuth authentication failed:" + authException);

    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Oauth authentication failed");


  }


}



OauthAuthenProvider.java

package sample.autho3.security.oauth;


import javax.servlet.http.HttpServletRequest;


import org.springframework.security.authentication.AuthenticationProvider;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.AuthenticationException;

import org.springframework.stereotype.Component;


import lombok.extern.slf4j.Slf4j;

import sample.autho3.security.CustomUserDetails;


/**

 * 

 */

@Slf4j

@Component

public class OauthAuthenProvider  implements AuthenticationProvider {


  

  @Override

  public Authentication authenticate(Authentication authenRequest) throws AuthenticationException {


    Authentication authenResponse;

    

    Object oauthTokenObject = authenRequest.getCredentials();

    

    log.info("oauthTokenObject="+oauthTokenObject);


    String oauthToken = (String)oauthTokenObject;


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

    CustomUserDetails principal = ThirdPartyAuthorizerSimulator.getUserDetails(oauthToken);


    if (null == principal) {

      authenResponse = authenRequest;


    } else {

      authenResponse = new OauthPreAuthenticatedSession(principal, oauthToken);

    }


    return authenResponse;

  }


  @Override

  public boolean supports(Class<?> authentication) {

    return authentication.equals(OauthPreAuthenticatedSession.class);

  }


  /**

   * @param req

   * @return -

   */

  public String resolveToken(HttpServletRequest req) {

    // DOC. Authorization is an allowed header at the API Gateway

    String ret;


    String bearerToken = req.getHeader("Authorization");

    

    log.info("Header Authorization = "+ bearerToken);

    

    if (bearerToken != null && bearerToken.startsWith("Bearer ")) {

      ret = bearerToken.substring(7, bearerToken.length());

    } else {

      ret = null;

    }

    return ret;

  }

  

  

}



OauthPreAuthenticatedSession.java


package sample.autho3.security.oauth;


import org.springframework.security.authentication.AbstractAuthenticationToken;

import org.springframework.security.core.SpringSecurityCoreVersion;


import sample.autho3.security.CustomUserDetails;


/**

 * OAuth implementation for pre-authenticated identifier.<br>

 * {@link org.springframework.security.core.Authentication} 

 * <br>

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

 * request."

 */

public class OauthPreAuthenticatedSession extends AbstractAuthenticationToken {


  private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;


  private final CustomUserDetails principal;


  /**

   * Credentials (the oauthToken)

   */

  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 oauthToken  The pre-authenticated Oauth token

   */

  public OauthPreAuthenticatedSession(String oauthToken) {

    super(null);

    this.principal = null;

    this.credentials = oauthToken;

  }


  /**

   * 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 explicit authorities

   * @param oauthToken OAuth token

   */

  public OauthPreAuthenticatedSession(CustomUserDetails aPrincipal, String oauthToken) {

    super(aPrincipal.getAuthorities());

    this.principal = aPrincipal;

    this.credentials = oauthToken;

    setAuthenticated(true);

  }


  /**

   * Getter OAuth token.

   */

  @Override

  public Object getCredentials() {

    return this.credentials;

  }


  /*- [pending: will need to be marked (at)NotNull when ready for it]

   * 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;

  }


}



OauthSecurityConfigurer.java

package sample.autho3.security.oauth;


import org.springframework.security.config.annotation.SecurityConfigurerAdapter;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.web.DefaultSecurityFilterChain;

import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


/**

 * It configures a filter for the OAuth token manual authentication

 */

public class OauthSecurityConfigurer extends

    SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {


  private OauthAuthenProvider oauthTokenProvider;


  /**

   * @param oauthTokenProvider

   */

  public OauthSecurityConfigurer(OauthAuthenProvider oauthTokenProvider) {

    this.oauthTokenProvider = oauthTokenProvider;

  }


  @Override

  public void configure(HttpSecurity http) throws Exception {

    // Filter that processes the jwtToken if, and only if, it's not the authen/token operation


    OauthTokenAuthenFilter customFilter = new OauthTokenAuthenFilter(oauthTokenProvider);


    http.exceptionHandling().authenticationEntryPoint(new OauthAuthenEntryPoint()).and()

        .addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);

  }


}



OauthTokenAuthenFilter.java

package sample.autho3.security.oauth;


import java.io.IOException;


import javax.servlet.FilterChain;

import javax.servlet.ServletException;

import javax.servlet.ServletRequest;

import javax.servlet.ServletResponse;

import javax.servlet.http.HttpServletRequest;


import org.springframework.security.core.Authentication;

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

import org.springframework.web.filter.GenericFilterBean;


import lombok.extern.slf4j.Slf4j;


/**

 * Picks up the oauthToken and registers the principal in the security context.

 */

@Slf4j

public class OauthTokenAuthenFilter extends GenericFilterBean {


  /*-*/

  private OauthAuthenProvider oauthTokenProvider;


  // private String tokenRequestPath;


  /**

   * @param oauthTokenProvider

   */

  public OauthTokenAuthenFilter(OauthAuthenProvider oauthTokenProvider) {

    this.oauthTokenProvider = oauthTokenProvider;

    // this.tokenRequestPath = tokenRequestPath;

  }


  @Override

  public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)

      throws IOException, ServletException {


    String oauthToken = this.oauthTokenProvider.resolveToken((HttpServletRequest) req);

    log.info("Oauth token = "+ oauthToken);


    triggerManualAuthentication(oauthToken);


    filterChain.doFilter(req, res);

  }


  /**

   * Register the 'authentication' in the security context.

   * 

   * @param oauthToken

   */

  public void triggerManualAuthentication(String oauthToken) {

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


    Authentication authenRequest = new OauthPreAuthenticatedSession(oauthToken);

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

    SecurityContextHolder.getContext().setAuthentication(authenResponse);


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

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

            .getPrincipal());


  }


}



ThirdPartyAuthorizerSimulator.java

package sample.autho3.security.oauth;


import java.util.ArrayList;

import java.util.Collection;


import org.springframework.security.core.GrantedAuthority;

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


import sample.autho3.security.CustomUserDetails;


/**

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

 * <br>

 * It uses different hard coded 'oauth token' for testing purposes:<br>

 * <br>

 * 

 * <pre>

 * oauthToken description<br>

 * t0         Granted authorities: SUPER (see role hierarchy in GlobalMethodSecurityConfig)<br>

 * t27        Granted authorities: ROLE_GESTOR

 * [other]    Non-valid sessionId

 * </pre>

 */

public class ThirdPartyAuthorizerSimulator {


  /** SUPER token for testing purposes. */

  public static final String TOKEN_TO = "t0";

  /** ROLE_GESTOR token for testing purposes. */

  public static final String TOKEN_T27 = "t27";


  /**

   * 

   * @param oauthToken (see class javadoc)

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

   */

  public static CustomUserDetails getUserDetails(String oauthToken) {

    if (TOKEN_TO.equals(oauthToken)) {


      long idp0 = 0L;


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

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


    } else if (TOKEN_T27.equals(oauthToken)) {

      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 'SUPER'

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

    } else if (27 == idp) {

      // DOC. Adding roles


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

    }


    return ret;

  }


}




REST controllers

AuthenController

The Authen contoller provides the operation for the invoker to exchange his OAuth token by the, application generated, JWT token.

package sample.autho3.facade;


import static org.springframework.http.ResponseEntity.ok;


import java.util.Collection;

import java.util.HashMap;

import java.util.Map;

import java.util.stream.Collectors;


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

import org.springframework.http.MediaType;

import org.springframework.http.ResponseEntity;

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

import org.springframework.security.authentication.BadCredentialsException;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.annotation.AuthenticationPrincipal;

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

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

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

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

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


import lombok.NonNull;

import sample.autho3.dto.UserOutDto;

import sample.autho3.security.CustomUserDetails;

import sample.autho3.security.jwt.JwtAuthenProvider;


/**

 * Controller used for getting an app JWT token.<br>

 * This JWT token will be needed for invoking operations in other controllers.

 */

@RestController

@RequestMapping(path = "/rest/authen", produces = MediaType.APPLICATION_JSON_VALUE)

public class AuthenController {


  /*- */

  private JwtAuthenProvider jwtTokenProvider;


  /**

   * If need to manually retrieve ReachableGrantedAuthorities

   */

  private RoleHierarchy roleHierarchy;


  /**

   * Constructor.

   * 

   * @param jwtTokenProvider Injected

   * @param roleHierarchy    -

   */

  @Autowired

  public AuthenController(JwtAuthenProvider jwtTokenProvider, RoleHierarchy roleHierarchy) {

    this.jwtTokenProvider = jwtTokenProvider;

    this.roleHierarchy = roleHierarchy;

  }


  /**

   * Exchange user OAuth token by a JWT one, plus user info.

   * 

   * <pre>

   * curl http://localhost:8080/r/w/authen/jwt -H "Authorization: Bearer t27"

   * </pre>

   * 

   * @param userDetails Framework injected Authentication.getPrincipal()

   * @return If succeeds, the app user JWT token & user details for the frontend

   */

  @Operation(summary = "Exchange OAuth token for JWT token", security = { @SecurityRequirement(

      name = "bearer-key") })

  @ApiResponses(value = { // NOSOSNAR

      @ApiResponse(responseCode = "200", description = "OK", content = @Content(

          mediaType = "application/json")), //

      @ApiResponse(responseCode = "401", description = "Unauthenticated (no OAuth token provided)",

          content = @Content(mediaType = "application/json")), //

      @ApiResponse(responseCode = "403",

          description = "User might not have the necessary permissions (OAuth token isn't valid)",

          content = @Content(mediaType = "application/json")) //

  })

  @PreAuthorize(SecurityHelper.HAS_AUTHENTICATED)

  @GetMapping(value = "/jwt", produces = MediaType.APPLICATION_JSON_VALUE)

  public ResponseEntity<Map<Object, Object>> jwt( // NOSONAR

      @Parameter(description = "Injected by framework",

          hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails) {


    // DOC. The JWT only holds explicitly granted authys (userAuhthysAll also has implicit ones)

    String jwt = jwtTokenProvider.createToken(userDetails);

    Collection<? extends GrantedAuthority> userAuhthysAll = SecurityHelper.getAuthoritiesAll();


    // INI If it's an student, ensure we have its 'niub' stored

    this.studentService.storeNiubIfApplicable(userDetails.getId(), userAuhthysAll);

    // END If it's an student, ensure we have its 'niub' stored


    Map<Object, Object> ret = new HashMap<>();

    ret.put("jwt", jwt);

    ret.put("userDetails", CustomUserDto.of(userDetails, userAuhthysAll));


    return ok(ret);

  }


  /**

   * 

   * @return All reachable roles (taking into account the authority hierarchy).

   */

  private Collection<? extends GrantedAuthority> reachableRoles(

      final Collection<? extends GrantedAuthority> userExplicitAuthorities) {

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

    @SuppressWarnings("unchecked")

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

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


    Collection<? extends GrantedAuthority> AuthoritiesAll = roleHierarchy

        .getReachableGrantedAuthorities(authorities);


    System.out.println("AuthoritiesAll: " + AuthoritiesAll);


    Collection<GrantedAuthority> rolesAll = AuthoritiesAll.stream().filter(sgu -> sgu.getAuthority()

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


    System.out.println("RolesAll: " + rolesAll);


    return rolesAll;

  }


}


SampleController

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 authority SUPER will be able to execute any method without expliciting it in the expression.

package sample.autho3.facade;


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


import javax.validation.constraints.NotNull;


import org.springframework.http.MediaType;

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

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 testing users (oauthTokens and authorities).<br>

 * Note: The JWT issued by 'AuthenController' is provided at the 'X-Api-Key' header.

 * 

 */

@RestController

@RequestMapping(path = "/rest/sample", produces = MediaType.APPLICATION_JSON_VALUE)

public class SampleController {


  /**

   * ROLE_LIBRARIAN does not exist.<br>

   * Remark: Remember to replace jwtXX by the JWT token previously obtained<br>

   * (b) t0 does not have role http://localhost:8080/rest/sample/role/librarian -H "X-Api-Key:

   * Bearer jwt0" (a) t27 does not have role<br>

   * curl http://localhost:8080/rest/sample/role/librarian -H "X-Api-Key: Bearer jwtt27"<br>

   * 

   * 

   * @param jwtToken

   * @return -

   */

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

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

  @ResponseBody

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

      required = true) @NotNull String jwtToken // NOSONAR

  ) {

    System.out.println("hola test0");

    return "{ret: 'KO'}\n";

  }


  /**

   * t027 has role ROLE_GESTOR<br>

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

   * 

   * @param jwtToken

   * @return -

   */

  @PreAuthorize("hasRole('GESTOR')")

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

  @ResponseBody

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

      required = true) @NotNull String jwtToken // NOSONAR

  ) {

    return "ok\n";

  }


  /**

   * 

   * t27 does NOT have role ROLE_MONITOR_APP (the authority is not a role)<br>

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

   * 

   * @param jwtToken

   * @return -

   */

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

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

  @ResponseBody

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

      required = true) @NotNull String jwtToken // NOSONAR

  ) {

    return "KO\n";

  }


  /**

   * 

   * t27 implicitly reaches ROLE_MEMBRE<br>

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

   * 

   * @param jwtToken

   * @return -

   */

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

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

  @ResponseBody

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

      required = true) @NotNull String jwtToken // NOSONAR

  ) {

    return "ok\n";

  }


  /**

   * 

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

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

   * 

   * @param jwtToken

   * @return -

   */

  @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 jwtToken // NOSONAR

  ) {

    return "KO\n";

  }


  /**

   * 

   * t0 has authority MONITOR_APP<br>

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

   * 

   * @param jwtToken

   * @return -

   */

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

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

  @ResponseBody

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

      required = true) @NotNull String jwtToken // NOSONAR

  ) {

    return "ok";

  }


  /**

   * 

   * t27 implicitly has authority ROLE_MEMBRE<br>

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

   * 

   * @param jwtToken

   * @return -

   */

  @PreAuthorize("hasAuthority('ROLE_MEMBRE')")

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

  @ResponseBody

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

      required = true) @NotNull String jwtToken // NOSONAR

  ) {

    return "ok\n";

  }


  /**

   * 

   * t27 does not have authority NON_EXISTENT<br>

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

   * 

   * @param jwtToken -

   * @return -

   */

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

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

  @ResponseBody

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

      required = true) @NotNull String jwtToken // NOSONAR

  ) {

    return "KO\n";

  }


  /**

   * 

   * t27 has at least one authority<br>

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

   * 

   * @param jwtToken

   * @return -

   */

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

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

  @ResponseBody

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

      required = true) @NotNull String jwtToken // NOSONAR

  ) {

    return "ok";

  }


  /**

   * 

   * t27 does not have any authority<br>

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

   * 

   * @param jwtToken

   * @return -

   */

  @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 jwtToken // NOSONAR

  ) {

    return "KO\n";

  }


  /**

   * Authorize only if username is 'root'.<br>

   * <br>

   * 

   * User with OAuth token 't0' has the intended username<br>

   * curl http://localhost:8080/rest/sample/username -H "X-Api-Key: Bearer HS5w0A0e"

   * 

   * @param jwtToken

   * @return -

   */

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

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

  @ResponseBody

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

      required = true) @NotNull String jwtToken // NOSONAR

  ) {

    return "{OK}\n";

  }


}