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:
The operation "/rest/authen/token" receives in the header 'Authorization' an OAuth token of an already authenticated user in a 3rd party authenticatio system, and it returns returns a JWT token (the invoker exchanges an OAuth token by a JWT token). That JWT token will be used for invoking any another operation, sending it in the header 'X-Api-Key'.
The application enforces manual authentication and custom authorization based on either the OAuth token or the JWT token (depending on the operation). When using OAuth, the custom authorization uses the authorities received from another 3rd party authorization system. When using JWT, custom authorization uses the authorities previously stored in the JWT token.
Furthermore, the application configures an authority hierarchy for easy resolving or reachable authorities..
References
Spring authorization
https://docs.spring.io/spring-security/site/docs/current/reference/html/authorization.html
How to Manually Authenticate User with Spring Security
https://www.baeldung.com/manually-set-user-authentication-spring-security
Implementing JWT Authentication on Spring Boot APIs
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";
}
}