Manual authentication, custom authorization w/ OAuth & hierarchical authorities configuration (Spring Boot)
Introduction
Parent page "Spring Security (Spring Boot)":
https://sites.google.com/site/pawneecity/sprint-boot/spring-security-spring-boot
This sample shows a Spring Boot 2.6.0 application that receives the token of an already authenticated user in a 3rd party authentication system.
The application enforces manual authentication and custom authorization based on the authorities received from another 3rd party authorization system.
Furthermore, the application configures an authority hierarchy for easy handling of the authorities returned by the 3rd party authorization system.
References
Spring Security > Authorization > Expression-Based Access Control
(current) https://docs.spring.io/spring-security/reference/servlet/authorization/expression-based.html
(5.5.x) https://docs.spring.io/spring-security/site/docs/5.5.x/reference/html5/#el-access
Intro to Spring Security Expressions
https://www.baeldung.com/spring-security-expressions
How to Manually Authenticate User with Spring Security
https://www.baeldung.com/manually-set-user-authentication-spring-security
Spring Security without the WebSecurityConfigurerAdapter (Spring Boot >= 2.7)
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
Spring Security - How to Fix WebSecurityConfigurerAdapter Deprecated (Spring Boot >= 2.7)
https://www.codejava.net/frameworks/spring-boot/fix-websecurityconfigureradapter-deprecated
POM configuration
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Security configuration
MethodSecurityConfig (3.3 <= Spring Boot)
package sample.autho2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
/**
* Configuration for setting a role hierarchy.
*
* <pre>
* Following is needed for PrePost work in controllers:
* (at)Configuration
* (at)EnableMethodSecurity(prePostEnabled = true)
* </pre>
*/
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig {
@Bean
RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.fromHierarchy( //
"ROLE_SUPER > ROLE_LIBRARIAN_ADMIN > ROLE_LIBRARIAN\n" //
+ "ROLE_SUPER > ROLE_SHOEMAKER\n" //
+ "ROLE_SUPER > ROLE_WRITER\n" //
+ "ROLE_SUPER > CREATE_REPORT\n" //
+ "ROLE_SUPER > EXTRACT_DATA\n" //
+ "ROLE_SUPER > MONITOR_APP\n" //
+ "ROLE_SUPER > IMPERSONATE_USER" //
);
}
/** Since Spring Boot 3.x (Spring Security 6.0), this bean replaces the previous RoleHierarchyVoter one. */
@Bean
DefaultMethodSecurityExpressionHandler expressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler
= new DefaultMethodSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy());
return expressionHandler;
}
}
MethodSecurityConfig (3.0 <= Spring Boot < 3.3)
package sample.autho2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
/**
* Configuration for setting a role hierarchy.
*
* <pre>
* Following is needed for PrePost work in controllers:
* (at)Configuration
* (at)EnableMethodSecurity(prePostEnabled = true)
* </pre>
*/
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig {
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy(//
"ROLE_SUPER > ROLE_LIBRARIAN_ADMIN > ROLE_LIBRARIAN\n" //
+ "ROLE_SUPER > ROLE_SHOEMAKER\n" //
+ "ROLE_SUPER > ROLE_WRITER\n" //
+ "ROLE_SUPER > CREATE_REPORT\n" //
+ "ROLE_SUPER > EXTRACT_DATA\n" //
+ "ROLE_SUPER > MONITOR_APP\n" //
+ "ROLE_SUPER > IMPERSONATE_USER" //
);
return roleHierarchy;
}
/** Since Spring Boot 3.x (Spring Security 6.0), this bean replaces the previous RoleHierarchyVoter one. */
@Bean
DefaultMethodSecurityExpressionHandler expressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler
= new DefaultMethodSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy());
return expressionHandler;
}
}
GlobalMethodSecurityConfig.java (Spring Boot < 3.0)
package sample.autho2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.access.vote.RoleHierarchyVoter;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
/**
*
* Configuration for setting a role hierarchy.<br>
*
* Following is needed for PrePost work in controllers:<br>
* (at)EnableGlobalMethodSecurity(prePostEnabled = true)
*
*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class GlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
/*- DOC. Since Spring Boot 2.6; it's detected as a circular reference.
@Autowired
private RoleHierarchy roleHierarchy
*/
/*- DOC. At Spring Boot 2.6; it's done by:
* GlobalMethodSecurityConfiguration.afterSingletonsInstantiated()
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() [
return methodSecurityExpressionHandler()
]
*/
/*- DOC. At Spring Boot 2.6; it's done by:
* GlobalMethodSecurityConfiguration.afterSingletonsInstantiated()
private DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() [
var expressionHandler = new DefaultMethodSecurityExpressionHandler()
expressionHandler.setRoleHierarchy(this.roleHierarchy)
return expressionHandler
]
*/
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy(//
"ROLE_SUPER > ROLE_LIBRARIAN_ADMIN > ROLE_LIBRARIAN\n" //
+ "ROLE_SUPER > ROLE_SHOEMAKER\n" //
+ "ROLE_SUPER > ROLE_WRITER\n" //
+ "ROLE_SUPER > CREATE_REPORT\n" //
+ "ROLE_SUPER > EXTRACT_DATA\n" //
+ "ROLE_SUPER > MONITOR_APP\n" //
+ "ROLE_SUPER > IMPERSONATE_USER" //
);
return roleHierarchy;
}
@Bean
public RoleHierarchyVoter roleVoter() {
return new RoleHierarchyVoter(roleHierarchy());
}
}
WebSecurityConfig.java (3.2 <= Spring Boot)
Eg: defense
package sample.autho32;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import sample.autho32.security.oauth.OauthAuthenEntryPoint;
import sample.autho32.security.oauth.OauthAuthenProvider;
import sample.autho32.security.oauth.OauthSecurityConfigurer;
import jakarta.inject.Inject;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
/** Data REST base path */
private String basePathDataRest;
/*- */
private OauthAuthenProvider oauthTokenProvider;
/**
* Constructor.
*
* @param basePathDataRest -
* @param oauthTokenProvider -
*/
@Inject
public WebSecurityConfig(
@Value("${spring.data.rest.base-path:'/rest/merida'}") String basePathDataRest, //
OauthAuthenProvider oauthTokenProvider//
) {
super();
this.basePathDataRest = basePathDataRest;
this.oauthTokenProvider = oauthTokenProvider;
}
/**
* Configuration for allowing special characters, e.g: ';', '\', '/' and '%'.
*
* @return -
*/
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
var firewall = new StrictHttpFirewall();
// symbol '/' encoded as '%2F' [didn't work w/ SpringBoot/3.1.3 that uses Tomcat/10.1.12]
firewall.setAllowUrlEncodedSlash(true);
/*- symbol '\'
firewall.setAllowBackSlash(true)
*/
/*- symbol ';'
firewall.setAllowSemicolon(true)
*/
/*- symbol '%' encoded as '%25'
firewall.setAllowUrlEncodedPercent(true)
*/
return web -> web.httpFirewall(firewall);
}
/**
* @param http
* @return -
* @throws Exception
*/
@SuppressWarnings({ "java:S1612" })
@Order(1)
@Bean
SecurityFilterChain filterChain1(HttpSecurity http) throws Exception {
http//
.securityMatcher(basePathDataRest, basePathDataRest + "/explorer/**") // Apply only if match
.cors(withDefaults())// exclude OPTIONS requests from authorization checks
// .and()//
.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()) //
.csrf(csrf -> csrf.disable()) //
.headers(headers -> headers.frameOptions(fo -> fo.disable()))//
// .and()//
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//
;
if (log.isDebugEnabled()) {
log.debug("END WebSecurityConfig.filterChain1");
}
return http.build();
}
/**
* @param http
* @return -
* @throws Exception
*/
@SuppressWarnings({ "java:S1612" }) // Unhandled token in '@SuppressWarnings': Ingore
@Order(2)
@Bean
SecurityFilterChain filterChain2(HttpSecurity http) throws Exception {
http// sb3
.securityMatcher(basePathDataRest + "/**")//
.authorizeHttpRequests(authorize -> authorize.anyRequest().hasAuthority("SUPER"))// NOSONAR
// .apply(new OauthSecurityConfigurer(oauthTokenProvider))
.with(new OauthSecurityConfigurer(oauthTokenProvider), withDefaults());
/*
* 'apply' For removal in 7.0. Use
* AbstractConfiguredSecurityBuilder.with(SecurityConfigurerAdapter, Customizer) instead.
*/
http.exceptionHandling(handling -> handling.authenticationEntryPoint(
new OauthAuthenEntryPoint()))//
.csrf(csrf -> csrf.disable())//
.headers(headers -> headers.frameOptions(fo -> fo.disable()))//
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//
;
if (log.isDebugEnabled()) {
log.debug("END WebSecurityConfig.filterChain2");
}
return http.build();
}
/**
* Actuator.<br>
* http://localhost:8080/rest/actuator<br>
* <br>
*
* @param http
* @return -
* @throws Exception
*/
@SuppressWarnings({ "java:S1612" })
@Order(3)
@Bean
SecurityFilterChain filterChain3(HttpSecurity http) throws Exception {
http// sb3
.securityMatcher("/rest/actuator", "/rest/actuator/**") //
.authorizeHttpRequests(authorize -> authorize.anyRequest().hasAuthority("SUPER"))// NOSONAR
// .apply(new OauthSecurityConfigurer(oauthTokenProvider))
.with(new OauthSecurityConfigurer(oauthTokenProvider), withDefaults());
/*
* 'apply' For removal in 7.0. Use
* AbstractConfiguredSecurityBuilder.with(SecurityConfigurerAdapter, Customizer) instead.
*/
http.exceptionHandling(handling -> handling.authenticationEntryPoint(
new OauthAuthenEntryPoint()))//
.csrf(csrf -> csrf.disable())//
.headers(headers -> headers.frameOptions(fo -> fo.disable()))//
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//
;
if (log.isDebugEnabled()) {
log.debug("END WebSecurityConfig.filterChain3");
}
return http.build();
}
/**
* Paths from coming from the APP API Gateway which do not require any additional security
* check:<br>
* -"/rest/export/**" > OAuth token already verified by API Gateway<br>
* -"/rest/monitor/**" > OAuth token already verified by API Gateway<br>
* -"/rest/public/**" > Public path<br>
*
* @param http
* @return -
* @throws Exception
*/
@SuppressWarnings({ "java:S1612" })
@Order(4)
@Bean
SecurityFilterChain filterChain4(HttpSecurity http) throws Exception {
if (log.isDebugEnabled()) {
log.debug("INI WebSecurityConfig.filterChain4");
}
http// sb3
.securityMatcher("/rest/export/**", "/rest/monitor/**", "/rest/public/**") //
.cors(withDefaults())//
.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()) //
.csrf(csrf -> csrf.disable())//
.headers(headers -> headers.frameOptions(fo -> fo.disable()))//
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//
;
if (log.isDebugEnabled()) {
log.debug("END WebSecurityConfig.filterChain4");
}
return http.build();
}
/**
* Security entry point for paths with OAuth token that require affiliations plus autho
* tables.<br>
* Note: It has order 5, therefore it will only execute if no adapter with order<5 matches.
*
* @param http
* @return -
* @throws Exception
*/
@SuppressWarnings({ "java:S1612" })
@Order(5)
@Bean
SecurityFilterChain filterChain5Oauth(HttpSecurity http) throws Exception {
if (log.isDebugEnabled()) {
log.debug("INI WebSecurityConfig.filterChain5Oauth");
}
http// sb3
.securityMatcher("/rest/**")//
.cors(withDefaults())//
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())// NOSONAR
// .apply(new OauthSecurityConfigurer(this.oauthTokenProvider))
.with(new OauthSecurityConfigurer(this.oauthTokenProvider), withDefaults());
/*
* 'apply' For removal in 7.0. Use
* AbstractConfiguredSecurityBuilder.with(SecurityConfigurerAdapter, Customizer) instead.
*/
http.exceptionHandling(handling -> handling.authenticationEntryPoint(
new OauthAuthenEntryPoint()))//
.csrf(csrf -> csrf.disable())//
.headers(headers -> headers.frameOptions(fo -> fo.disable()))//
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//
;
if (log.isDebugEnabled()) {
log.debug("END WebSecurityConfig.filterChain5Oauth");
}
return http.build();
}
/**
* Springdoc v2 (for Spring Boot >= 3.0).
*
* @param http
* @return -
* @throws Exception
*/
@SuppressWarnings({ "java:S1612", "java:S6212" })
@Order(6)
@Bean
SecurityFilterChain filterChainSpringDocV2(HttpSecurity http) throws Exception {
final String[] pathAnonymous = // NOSONAR (SONARJAVA-3991)
{ "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**" };
http// sb3
.securityMatcher(pathAnonymous) // only invoke if matching
.authorizeHttpRequests(authorize -> authorize//
.anyRequest().anonymous()//
) //
;
http.csrf(csrf -> csrf.disable())//
.headers(headers -> headers.frameOptions(fo -> fo.disable()));//
// DOC. Avoid generation of cookie JSESSIONID (Swagger)
http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
/**
* Paths which aren't exposed in the App API Gateway and therefore only usable directly:<br>
*
* <pre>
* /h2-console/**
* /sub2/** (subprocess 2) *** !!!
* </pre>
*
* @param http
* @return -
* @throws Exception
*/
@SuppressWarnings({ "java:S1612" })
@Order(7)
@Bean
SecurityFilterChain filterChainNoApiGtw(HttpSecurity http) throws Exception {
http// sb3
.securityMatcher("/h2-console/**", "/sub2/**") //
.cors(withDefaults()) //
.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()) //
.csrf(csrf -> csrf.disable())//
.headers(headers -> headers.frameOptions(fo -> fo.disable()))//
.sessionManagement(management -> management.sessionCreationPolicy(
SessionCreationPolicy.STATELESS))//
;
if (log.isDebugEnabled()) {
log.debug("END WebSecurityConfig.filterChainNoApiGtw");
}
return http.build();
}
/**
* Other anonymous.
*
* @param http
* @return -
* @throws Exception
*/
@SuppressWarnings({ "java:S1612", "java:S6212" })
@Order(7)
@Bean
SecurityFilterChain filterChainOtherAnonymous(HttpSecurity http) throws Exception {
final String[] pathAnonymous = { "/ui/**", "/monitor/**", "/tech/**", "/favicon*", "/version",
"/csrf" };
// , "/webjars/**", "/swagger-resources/**", "/", "/v2/api-docs/**"
http// sb3
.securityMatcher(pathAnonymous) // Apply only if match
.authorizeHttpRequests(authorize -> authorize//
.anyRequest().anonymous()//
) //
;
/*- sb3
http // NOSONAR
.authorizeRequests().antMatchers(pathAnonymous).permitAll() // NOSONAR
*/
http.csrf(csrf -> csrf.disable())//
.headers(headers -> headers.frameOptions(fo -> fo.disable()));
http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
WebSecurityConfig.java (Spring Boot < 3.2)
package sample.autho2;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Data REST HAL Explorer didn't work fine on AWS because links are generated with private load
* balancer address, instead of the public CloudFront one; although Data REST works fine.
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/** Data REST base path */
public static final String BASE_PATH_DATA_REST = "/rest/merida";
/**
* Spring Boot >= 3.0 equivalence of the configure method
@Order(6)
@Bean
SecurityFilterChain filterChain6(HttpSecurity http) throws Exception {
http//
.authorizeHttpRequests().requestMatchers("/", "/home", "/*", "/h2-console/**", BASE_PATH_DATA_REST + "/explorer/**").permitAll()//
.cors()// exclude OPTIONS requests from authorization checks
.and()//
.csrf().disable().headers().frameOptions().disable()//
.and()//
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//
;
return http.build();
}
*/
/**
* Spring Boot >= 2.7 <3.0 equivalence of the configure method
@Order(6)
@Bean
SecurityFilterChain filterChain6(HttpSecurity http) throws Exception {
http.requestMatchers().antMatchers("/", "/home", "/*", "/h2-console/**", BASE_PATH_DATA_REST + "/explorer/**") //
.and()//
.cors()// exclude OPTIONS requests from authorization checks
.and() //
.authorizeRequests().anyRequest().permitAll() //
.and()//
.csrf().disable().headers().frameOptions().disable()//
.and()//
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//
;
return http.build();
}
*/
/** configure method for Spring Boot < 2.7 */
@Override
protected void configure(HttpSecurity http) throws Exception {
http // NOSONAR
.authorizeRequests() // NOSONAR
.antMatchers("/", "/home", "/*").permitAll() // NOSONAR
.antMatchers("/h2-console/**").permitAll() // NOSONAR
.antMatchers(BASE_PATH_DATA_REST + "/explorer/**").permitAll() // NOSONAR
;
http.csrf().disable();
http.headers().frameOptions().disable();
// DOC. Avoid generation of cookie JSESSIONID (Swagger)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
Manual authentication
Since the user is already authenticated in a 3rd party system, we'll manually authenticate the user in Spring Boot so that the framework can enforce authorization with it.
The idea is to intercept all the http requests to the REST controller, and register in the security context the authentication based on the http header X-Api-Key of the request that holds the token provided by the 3rd party authentication system.
CustomInvalidAuthenExceptionTest.java
package sample.autho2.exception;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.security.core.AuthenticationException;
/**
* Exception related to an Authentication object being invalid for whatever reason.
*
* Note: This class extends the Spring Boot AuthenticationException (not a custom one).
*
* 401 'Unauthenticated' http status code.
*
* Note that, semantically, http defines two different codes:<br>
* ·401 for 'unauthenticated' (user does not have a valid session)<br>
* ·403 for 'unauthorized' (user has a valid session, but he's not authorized)<br>
*/
public class CustomInvalidAuthenException extends AuthenticationException {
private static final long serialVersionUID = -371528602897747414L;
/** REST response suggested http status code. */
public static final HttpStatus HTTP_STATUS = HttpStatus.UNAUTHORIZED;
/**
* Constructor.
*
* @param message -
*/
public CustomInvalidAuthenException(final String message) {
this(message, null);
}
/**
* Constructor
*
* @param message .
* @param cause Nullable
*/
public CustomInvalidAuthenException(final String message, final @Nullable Throwable cause) {
super(message, cause);
}
}
CustomInvalidAuthenExceptionTest.java
package sample.autho2.exception;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import lombok.val;
/**
* .
*/
class CustomInvalidAuthenExceptionTest {
/***/
@Test
void allTest() {
assertEquals(HttpStatus.UNAUTHORIZED, CustomInvalidAuthenException.HTTP_STATUS);
val i = new CustomInvalidAuthenException("I'm a unit test");
assertNotNull(i);
}
}
CustomAuthenInterceptor.java is in charge of intercepting all http requests and of registering the 'Authentication' instance in the security context
package sample.autho2.facade;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
/**
* User authentication interceptor.<br>
* <br>
*
* (at)ComponentScan needed to be able to inject CustomAuthenProvider.<br>
* (at)Configuration unused because Bean created by CustomAuthenConfig<br>
*/
@ComponentScan
public class CustomAuthenInterceptor extends HandlerInterceptorAdapter {
private static final Logger log = LoggerFactory.getLogger(CustomAuthenInterceptor.class);
protected static final String HEADER_SESSION = "Authorization";
private LocaleResolver localeResolver;
private CustomAuthenProvider authenProvider;
/**
* @param localeResolver
* @param userDetailsService
*/
@Autowired
public CustomAuthenInterceptor(final @NotNull LocaleResolver localeResolver,
final @NotNull UserDetailsService userDetailsService) {
super();
this.localeResolver = localeResolver;
this.authenProvider = new CustomAuthenProvider(userDetailsService);
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
log.info("Request intercepted, triggering manual authentication");
final String headerAuthorization = request.getHeader(HEADER_SESSION);
final String oauthToken;
if (headerAuthorization == null || !headerAuthorization.startsWith(
SecurityHelper.AUTHEN_BEARER_PREFIX)) {
oauthToken = null;
} else {
oauthToken = headerAuthorization.substring(SecurityHelper.AUTHEN_BEARER_PREFIX.length());
}
final Locale l = this.localeResolver.resolveLocale(request);
this.triggerManualAuthentication(oauthToken, l.getLanguage());
return true;
}
/**
* Register the 'authentication' in the security context.
*
* @param tokenOauth OAuth token
* @param l Language ISO 639-1 code.
*/
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public void triggerManualAuthentication(final String tokenOauth, final @NotNull String l) {
log.info("INI Manual authentication...");
Authentication authenRequest = new CustomPreAuthenticatedSession(tokenOauth);
Authentication authenResponse = this.authenProvider.authenticate(authenRequest);
// Set user language
if (authenResponse.getPrincipal() != null) {
((CustomUserDetails) authenResponse.getPrincipal()).setLangIso639p1Code(l);
}
//
SecurityContext sc = SecurityContextHolder.getContext();
sc.setAuthentication(authenResponse);
log.info("END Manual authentication.\nisAuthenticated=" + authenResponse.isAuthenticated()
+ "\nAuthorities=" + authenResponse.getAuthorities() + "\nPrincipal=" + authenResponse
.getPrincipal());
}
}
CustomAuthenProvider.java is in charge of obtaining the Authorization instance to be registered in the security context
package sample.autho2.facade;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
//import org.springframework.stereotype.Component;
/*-@Component*/
public class CustomAuthenProvider implements AuthenticationProvider {
/*-
* @return a fully authenticated object including credentials. May return
* <code>null</code> if the <code>AuthenticationProvider</code> is unable to support
* authentication of the passed <code>Authentication</code> object. In such a case,
* the next <code>AuthenticationProvider</code> that supports the presented
* <code>Authentication</code> class will be tried.
* @throws AuthenticationException if authentication fails.
*/
@Override
@Nullable
public Authentication authenticate(Authentication authenRequest) throws AuthenticationException {
Authentication authenResponse;
var /* Object */ oauthTokenObject = authenRequest.getCredentials();
String oauthToken;
try {
oauthToken = (String) oauthTokenObject;
}catch(ClassCastException cce) {
// This <code>AuthenticationProvider</code> is unable to support authentication of the passed <code>Authentication</code> object
return null;
}
// DOC Use the credentials for retrieving user details from 3rd party system
CustomUserDetails principalNullable = this.ubOauthAuthorizerService.getUserDetails(oauthToken);
if (null == principalNullable) {
// Provided 'oauthToken' is not valid
//throw new CustomInvalidAuthenException("Provided 'oauthToken' is not valid")
authenResponse = authenRequest;
} else {
authenResponse = new OauthPreAuthenticatedSession(principalNullable, oauthToken);
}
return authenResponse;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(CustomPreAuthenticatedSession.class);
}
}
CustomAuthenConfig.java is in charge of registering the request interceptor for all requests to path "/user/**"
package sample.autho2.facade;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Configure request interceptor for path "/rest/**".
*/
@Configuration
public class CustomAuthenConfig implements WebMvcConfigurer {
private LocaleResolver localeResolver;
private UserDetailsService userDetailsService;
/**
* Constructor.
*
* @param localeResolver -
*
* @param userDetailsService -
*/
@Autowired
public CustomAuthenConfig(final @NotNull LocaleResolver localeResolver,
final @NotNull UserDetailsService userDetailsService) {
super();
this.localeResolver = localeResolver;
this.userDetailsService = userDetailsService;
}
/**
* If you're using Spring Data REST in your project, and you want to intercept your Spring
* Repositories and @RepositoryRestControllers, you need to define this @Bean method.<br>
* <br>
* This method must be declared in any @Configuration annotated class; and such class does not
* need to implement/extend anything special. The interceptor implementation must either implement
* HandlerInterceptor or extend HandlerInterceptorAdapter.<br>
*
* @return -
*/
@Bean
public MappedInterceptor customAuthenInterceptor() {
//[a] Secure APIs
String[] includePatterns = { "/rest/**" };
//[b] Allow public
String[] excludePatterns = { "/rest/public/**" };
return new MappedInterceptor(includePatterns, excludePatterns, new CustomAuthenInterceptor(
this.localeResolver, this.userDetailsService));
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this.customAuthenInterceptor());
}
/**
* CORS values that our application has
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "POST", "DELETE", "PATCH", "PUT", "OPTIONS")
.allowedHeaders("*");
}
}
CustomPreAuthenticatedSession.java is a custom implementation for the pre-authenticated session id (generated by the 3rd party authentication system)
package sample.autho2.facade;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.SpringSecurityCoreVersion;
/**
* {@link org.springframework.security.core.Authentication} implementation for custom
* pre-authenticated session identifier.
* <br>
* From that javadoc: "Callers are expected to populate the principal for an authentication
* request."
*/
public class CustomPreAuthenticatedSession extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final CustomUserDetails principal;
/**
* Credentials (the sessionId)
*/
private final String credentials;
/**
* Constructor used for an authentication request. The
* {@link org.springframework.security.core.Authentication#isAuthenticated()} will return
* <code>false</code>.
*
* @param aPrincipal The pre-authenticated principal
* @param sessionId The pre-authenticated session id
*/
public CustomPreAuthenticatedSession(String sessionId) {
super(null);
this.principal = null;
this.credentials = sessionId;
}
/**
* Constructor used for an authentication response. The
* {@link org.springframework.security.core.Authentication#isAuthenticated()} will return
* <code>true</code>.
*
* @param aPrincipal The authenticated principal, with its authorities
*/
public CustomPreAuthenticatedSession(CustomUserDetails aPrincipal, String sessionId) {
super(aPrincipal.getAuthorities());
this.principal = aPrincipal;
this.credentials = sessionId;
setAuthenticated(true);
}
/**
* Getter sessionId.
*/
@Override
public Object getCredentials() {
return this.credentials;
}
/*-
* The identity of the principal being authenticated. Callers are
* expected to populate the principal for an authentication request.
* <p>
* The <tt>AuthenticationManager</tt> implementation will often return an
* <tt>Authentication</tt> containing richer information as the principal for use by
* the application. Many of the authentication providers will create a
* {@code UserDetails} object as the principal.
*
* @return the <code>Principal</code> being authenticated or the authenticated
* principal after authentication.
*/
@Override
public CustomUserDetails getPrincipal() {
return this.principal;
}
}
CustomUserDetails.java extends the framework class UserDetails, adding the application specific user info returned by 3rd party system based on the session id.
package sample.autho2.facade;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.validation.constraints.NotNull;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.Assert;
/**
* Custom user details retrieved from 3rd party system.<br>
* <br>
* Example of response from 3rd party system:<br>
*
* <pre>
<out>
<extendedSessionCampus>
<appId>INST</appId>
<appIdTREN>INST</appIdTREN>
<iso639Language>en</iso639Language>
<languageId>c</languageId>
<loginTime>2020-01-12T11:24:38+01:00</loginTime>
<userDni>36241169T</userDni>
<userName>First Name</userName>
<userNumber>272876</userNumber>
<userSex>M</userSex>
<userSubTypeId>inst</userSubTypeId>
<userSurname1>Lastname1</userSurname1>
<userSurname2>Lastname2</userSurname2>
<userTypeId>CONS</userTypeId>
</extendedSessionCampus>
<idp>272876</idp>
<sessionId>598612657</sessionId>
<userLogin>username</userLogin>
</out>
* </pre>
*/
public class CustomUserDetails implements UserDetails {
/**
*
*/
private static final long serialVersionUID = 4206778189244514942L;
/**
* 3rd party system attribute 'userLogin'.<br>
*/
private final String username;
/**
* 3rd party system authorities, retrieved via operation getUserRoles(idp, module).<br>
* Note that a role is just a type of authority that begins with "ROLE_".<br>
*
* NonNull. Lazy loading, do always use getter!
*/
private final Set<GrantedAuthority> authorities;
/**
* 3rd party system returns userName + " " + userSurname1 + " " + userSurname2.
*
*/
@NotNull
private String displayName; // NOSONAR
/**
* Idp of the user.<br>
* Note: Since release 2.4, it's used by front-end because of inc. vgpra-294
*/
@NotNull
private Long idp = Long.valueOf(0); // NOSONAR
/**
* Language code, eg: 'ca', 'en' or 'es' (retrieved from 3rd party system).
*/
@NotNull
private String langIso639p1Code; // NOSONAR
/**
* Constructor.
*
* @param username
* @param password
* @param enabled
* @param accountNonExpired
* @param credentialsNonExpired
* @param accountNonLocked
* @param authorities
*/
public CustomUserDetails(String username, Collection<? extends GrantedAuthority> authorities,
String displayName, long idp, String langIso639p1Code) {
if (((username == null) || "".equals(username)) || (displayName == null)) {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
// DOC. Attributes needed because of extending UserDetails
this.username = username;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
// DOC. Custom attributes
this.displayName = displayName;
this.idp = Long.valueOf(idp);
this.langIso639p1Code = langIso639p1Code;
}
/**
* Cannot return <code>null</code>.
*/
@Override
public String getUsername() {
return this.username;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public String toString() {
return "["+username+", "+displayName+", "+idp+", "+langIso639p1Code+"]";
}
private static SortedSet<GrantedAuthority> sortAuthorities(
Collection<? extends GrantedAuthority> authorities) {
Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
// Ensure array iteration order is predictable (as per
// UserDetails.getAuthorities() contract and SEC-717)
SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet<>(new AuthorityComparator());
for (GrantedAuthority grantedAuthority : authorities) {
Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements");
sortedAuthorities.add(grantedAuthority);
}
return sortedAuthorities;
}
private static class AuthorityComparator implements Comparator<GrantedAuthority>, Serializable {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
public int compare(GrantedAuthority g1, GrantedAuthority g2) {
// Neither should ever be null as each entry is checked before adding it to
// the set.
// If the authority is null, it is a custom authority and should precede
// others.
if (g2.getAuthority() == null) {
return -1;
}
if (g1.getAuthority() == null) {
return 1;
}
return g1.getAuthority().compareTo(g2.getAuthority());
}
}
}
ThirdPartyAuthorizerSimulator.java simulates 3rd party system responses with user details and authorities (for session ids 's0' and 's27)
package sample.autho2.facade;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
/**
* It simulates responses from a 3rd party Authorization system.<br>
* <br>
* It uses 3 hardcoded sessionIds for testing purposes:<br>
* <br>
*
* <pre>
* sessionId description
* s0 Granted authority ROLE_SUPER (see role hierarchy in GlobalMethodSecurityConfig)
* s27 Granted authorities (eg: ROLE_LIBRARIAN, MONITOR_APP)
* [other] Non-valid sessionId
* </pre>
*/
public class ThirdPartyAuthorizerSimulator {
/**
*
* @param sessionId (only "s27" is considered valid)
* @return User details if sessionId is valid, null otherwise
*/
public static CustomUserDetails getUserDetails(String sessionId) {
if ("s0".equals(sessionId)) {
long idp0 = 0L;
return new CustomUserDetails("root", ThirdPartyAuthorizerSimulator.getUserAuthorities(idp0),
"Name zero", idp0, "en");
} else if ("s27".equals(sessionId)) {
String username = "user27";
String displayName = "Name 27";
long idp = 27;
String langCode = "pl";
CustomUserDetails ret = new CustomUserDetails(username, ThirdPartyAuthorizerSimulator
.getUserAuthorities(idp), displayName, idp, langCode);
return ret;
} else {
return null;
}
}
/**
* Remark: There is a role hierarchy configured in:<br>
* GlobalMethodSecurityConfig
*
* @param idp
* @return
*/
private static Collection<? extends GrantedAuthority> getUserAuthorities(long idp) {
Collection<SimpleGrantedAuthority> ret = new ArrayList<SimpleGrantedAuthority>();
if (0 == idp) {
// Simulate user with ROLE_SUPER
ret.add(new SimpleGrantedAuthority("ROLE_SUPER"));
// (by hierarchy)ret.add(new SimpleGrantedAuthority("MONITOR_APP"));
// (by hierarchy)ret.add(new SimpleGrantedAuthority("IMPERSONATE_USER"));
} else if (27 == idp) {
// DOC. Adding roles
ret.add(new SimpleGrantedAuthority("ROLE_SHOEMAKER"));
ret.add(new SimpleGrantedAuthority("ROLE_LIBRARIAN"));
ret.add(new SimpleGrantedAuthority("ROLE_WRITER"));
ret.add(new SimpleGrantedAuthority("ROLE_LIBRARIAN_ADMIN"));
// DOC. Adding other permissions
ret.add(new SimpleGrantedAuthority("CREATE_REPORT"));
ret.add(new SimpleGrantedAuthority("EXTRACT_DATA"));
}
return ret;
}
}
Sample REST controller
This controller shows how to enforce authorization with @PreAuthorize and expressions like hasRole, hasAuthority or principal.
Note that because of he authority hierarchical configuration, a user with role ROLE_SUPER will be able to execute any method without expliciting it in the expression (it can be retrieved with the operation '/user/authorities').
package sample.autho2.facade;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import java.util.Collection;
import java.util.stream.Collectors;
import javax.validation.constraints.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
* See ThirdPartyAuthorizerSimulator for mapping between sessionId (s27) and authorities
* (ROLE_LIBRARIAN, MONITORAPP).
*
*/
@RequestMapping(path = "/user")
@RestController
public class GeneralUserController {
/**
* If need to manually retrieve ReachableGrantedAuthorities
*/
private RoleHierarchy roleHierarchy;
/**
* Constructor.
*/
@Autowired
public GeneralUserController(@NotNull RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
}
/**
* Find out all reacheable granted authorities of the user.<br>
* <br>
* (a) sessionId 's27'<br>
* curl http://localhost:8080/user/authorities -H "X-Api-Key: s27" <br>
* <br>
* (b) sessionId 's0'<br>
* curl http://localhost:8080/user/authorities -H "X-Api-Key: s0"
*/
@PreAuthorize("permitAll()")
@RequestMapping(value = "/authorities", method = GET)
@ResponseBody
public String getAuthorities(@RequestHeader(name = "X-Api-Key",
required = true) @NotNull String sessionId // NOSONAR
) {
System.out.println("INI getAuthorities");
@SuppressWarnings("unchecked")
Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) SecurityContextHolder
.getContext().getAuthentication().getAuthorities();
Collection<? extends GrantedAuthority> hierarchyAuthorities = roleHierarchy
.getReachableGrantedAuthorities(authorities);
// System.out.println("Authorities: " + hierarchyAuthorities);
Collection<GrantedAuthority> roles = hierarchyAuthorities.stream().filter(sgu -> sgu
.getAuthority().startsWith("ROLE_")).collect(Collectors.toList());
// System.out.println("Roles: " + roles);
return "\nAuthorities: " + hierarchyAuthorities + "\nRoles: " + roles + "\n";
}
/**
* (a) s27 has role ROLE_LIBRARIAN<br>
* curl http://localhost:8080/user/role/librarian -H "X-Api-Key: s27" <br>
* (b) sessionId 's0' has ROLE_SUPER, which indirectly contains ROLE_LIBRARIAN<br>
* curl http://localhost:8080/user/role/librarian -H "X-Api-Key: s0"
*/
@PreAuthorize("hasRole('ROLE_LIBRARIAN')")
@RequestMapping(value = "/role/librarian", method = GET)
@ResponseBody
public String getRoleLibrarian(@RequestHeader(name = "X-Api-Key",
required = true) @NotNull String sessionId // NOSONAR
) {
System.out.println("INI getRoleLibrarian");
return "ok\n";
}
/**
* Session s27 does not have ROLE_SUPER<br>
* curl http://localhost:8080/user/role/super -H "X-Api-Key: s27" <br>
*/
@PreAuthorize("hasRole('ROLE_SUPER')")
@RequestMapping(value = "/role/super", method = GET)
@ResponseBody
public String getRoleSuper(@RequestHeader(name = "X-Api-Key",
required = true) @NotNull String sessionId // NOSONAR
) {
return "KO\n";
}
/**
* s027 has role ROLE_LIBRARIAN<br>
* curl http://localhost:8080/user/role/librarianWithoutPrefix -H "X-Api-Key: s27"
*/
@PreAuthorize("hasRole('LIBRARIAN')")
@RequestMapping(value = "/role/librarianWithoutPrefix", method = GET)
@ResponseBody
public String getRoleLibrarianWithoutPrefixn(@RequestHeader(name = "X-Api-Key",
required = true) @NotNull String sessionId // NOSONAR
) {
return "ok\n";
}
/**
*
* s027 does NOT have role MONITOR_APP (the authority is not a role)<br>
* curl http://localhost:8080/user/role/monitor -H "X-Api-Key: s27"
*/
@PreAuthorize("hasRole('MONITOR_APP')")
@RequestMapping(value = "/role/monitor", method = GET)
@ResponseBody
public String getRoleMonitor(@RequestHeader(name = "X-Api-Key",
required = true) @NotNull String sessionId // NOSONAR
) {
return "KO\n";
}
/**
*
* s027 has role ROLE_SHOEMAKER<br>
* curl http://localhost:8080/user/role/anyYes -H "X-Api-Key: s27"
*/
@PreAuthorize("hasAnyRole('NON_EXISTENT','SHOEMAKER','MONITOR_APP')")
@RequestMapping(value = "/role/anyYes", method = GET)
@ResponseBody
public String getRoleAnyYes(@RequestHeader(name = "X-Api-Key",
required = true) @NotNull String sessionId // NOSONAR
) {
return "ok\n";
}
/**
*
* s027 does not have any of specified roles<br>
* curl http://localhost:8080/user/role/anyNo -H "X-Api-Key: s27"
*/
@PreAuthorize("hasAnyRole('NON_EXISTENT','ROLE_NON_EXISTENT','CREATE_REPORT')")
@RequestMapping(value = "/role/anyNo", method = GET)
@ResponseBody
public String getRoleAnyNo(@RequestHeader(name = "X-Api-Key",
required = true) @NotNull String sessionId // NOSONAR
) {
return "KO\n";
}
/**
*
* s027 has authority MONITOR_APP<br>
* curl http://localhost:8080/user/authority/monitor -H "X-Api-Key: s27"
*/
@PreAuthorize("hasAuthority('MONITOR_APP')")
@RequestMapping(value = "/authority/monitor", method = GET)
@ResponseBody
public String getAuthorityMonitor(@RequestHeader(name = "X-Api-Key",
required = true) @NotNull String sessionId // NOSONAR
) {
return "ok";
}
/**
*
* s027 has authority ROLE_LIBRARIAN<br>
* curl http://localhost:8080/user/authority/librarian -H "X-Api-Key: s27"
*/
@PreAuthorize("hasAuthority('ROLE_LIBRARIAN')")
@RequestMapping(value = "/authority/librarian", method = GET)
@ResponseBody
public String getAuthorityLibrarian(@RequestHeader(name = "X-Api-Key",
required = true) @NotNull String sessionId // NOSONAR
) {
return "ok\n";
}
/**
*
* s027 does not have authority NON_EXISTENT<br>
* curl http://localhost:8080/user/authority/nonExistent -H "X-Api-Key: s27"
*/
@PreAuthorize("hasAuthority('NON_EXISTENT')")
@RequestMapping(value = "/authority/nonExistent", method = GET)
@ResponseBody
public String getAuthorityNonexistent(@RequestHeader(name = "X-Api-Key",
required = true) @NotNull String sessionId // NOSONAR
) {
return "KO\n";
}
/**
*
* s027 has at least one authority<br>
* curl http://localhost:8080/user/authority/anyYes -H "X-Api-Key: s27"
*/
@PreAuthorize("hasAnyAuthority('NON_EXISTENT','ROLE_WRITER','MONITOR_APP')")
@RequestMapping(value = "/authority/anyYes", method = GET)
@ResponseBody
public String getAuthorityAnyYes(@RequestHeader(name = "X-Api-Key",
required = true) @NotNull String sessionId // NOSONAR
) {
return "ok";
}
/**
*
* s027 does not have any authority<br>
* curl http://localhost:8080/user/authority/anyNo -H "X-Api-Key: s27"
*/
@PreAuthorize("hasAnyAuthority('LIBRARIAN','ROLE_NON_EXISTENT','ROLE_MONITOR_APP')")
@RequestMapping(value = "/authority/anyNo", method = GET)
@ResponseBody
public String getAuthorityAnyNo(@RequestHeader(name = "X-Api-Key",
required = true) @NotNull String sessionId // NOSONAR
) {
return "KO\n";
}
/**
* Authorize ony if username is 'user27'.<br>
* <br>
*
* sessionId s27 has the intended username<br>
* curl http://localhost:8080/user/username -H "X-Api-Key: s27"
*/
@PreAuthorize("principal.username == 'user27'")
@RequestMapping(value = "/username", method = GET)
@ResponseBody
public String getUsername(@RequestHeader(name = "X-Api-Key",
required = true) @NotNull String sessionId // NOSONAR
) {
return "ok\n";
}
}