Validate the api with user defined roles in spring boot oauth resource server - spring-boot

I am currently working on resource server implemented by the spring boot o auth 2.0. Spring boot version would be 3.0.0. Need to authrorize the api with user-authorities. I tried below samples.
https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/opaque-token.html. But did n't get the user-authorities attribute. Can anyone sugget me the approach to to validate api with user roles in #PreAuthorize anotations.
Sample token format :
{
"sub": "admin#support.com",
"aud": "client",
"user-authorities": [
"READ_PRIVILEGE",
"WRITE_PRIVILEGE"
],
"azp": "client",
"iss": "http://localhost:8080",
"exp": 1676210945,
"iat": 1676209145
}

Two options to override default authorities mapping in case of token introspection (read user-authorities claim in your case instead of scope).
override the all OpaqueTokenIntrospector, as explained in the doc you linked
override just the OpaqueTokenAuthenticationConverter to which the default introspector delegates the conversion from successful introspection result to an Authentication instance
The second option is less intrusive as you don't take responsibility of how introspection is made (which REST client is used, how client authentication is performed, query built, response parsed, exceptions handled, etc.).
Sample usage:
#Bean
OpaqueTokenAuthenticationConverter introspectionAuthenticationConverter() {
return (String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) -> {
// double check the claim name to extract authorities from and if a prefix should be added:
// in the sample provided in your question, it is "user-authorities" claim
// but in the sample linked in your comment, it is "scope" claim and "SCOPE_" prefix should be added for "hasAuthority("SCOPE_openid")" to be matched
#SuppressWarnings("unchecked")
final var rolesClaim = Optional.ofNullable((List<String>) authenticatedPrincipal.getAttribute("user-authorities")).orElse(List.of());
return new BearerTokenAuthentication(
authenticatedPrincipal,
new OAuth2AccessToken(
TokenType.BEARER,
introspectedToken,
authenticatedPrincipal.getAttribute(IdTokenClaimNames.IAT),
authenticatedPrincipal.getAttribute(IdTokenClaimNames.EXP)),
// You should insert a ".map()" step to insert a prefix if access-control rules expect one (that is not visible in the access token)
rolesClaim.stream().map(SimpleGrantedAuthority::new).toList());
};
}
#Bean
SecurityFilterChain securityFilterChain(
HttpSecurity http,
OpaqueTokenAuthenticationConverter introspectionAuthenticationConverter)
throws Exception {
http.oauth2ResourceServer().opaqueToken().authenticationConverter(introspectionAuthenticationConverter);
...
return http.build();
}
Complete sample there with one of "my" starters which make things much simpler (check security conf and properties file against what you have in yours).

Related

Spring SAML: Multiple ACS URLs?

I'm trying to configure Spring SAML to work with multiple ACS URLs. I'd like the ACS URL to be determined based on some input the user provides, and it will select one of two ACS urls.
For example:
The user passes in a value A in the request, the ACS URL will be http://server1.com/saml/response.
The user passes in a value B in the request, the ACS URL will be http://server2.com/saml/response in the SAML Response
Any ideas or pointers in the right direction would be appriciated.
You don't specify what version of Spring Security SAML you're using. This is an example based on 1.0.10.RELEASE and is available here.
This is one way to do it:
public class ConfigurableWebSsoProfile extends WebSSOProfileImpl {
#Override
protected AuthnRequest getAuthnRequest(final SAMLMessageContext context,
final WebSSOProfileOptions options,
final AssertionConsumerService acs,
final SingleSignOnService bindingService)
throws SAMLException, MetadataProviderException {
AuthnRequest request = super.getAuthnRequest(context, options,
acs, bindingService);
if (something == true) {
request.setAssertionConsumerServiceURL(...);
} else {
request.setAssertionConsumerServiceURL(...);
}
return request;
}
}

Migrating away from Spring Security OAuth 2

I'm having a Spring Boot Auth Microservice. It uses the Oauth2 spring cloud starter dependency which is deprecated nowadays.
buildscript {
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.1.9.RELEASE"
}
}
dependencies {
implementation "org.springframework.boot:spring-boot-starter-actuator"
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.cloud:spring-cloud-starter-oauth2:2.1.5.RELEASE"
}
The Schema was taken from here: https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
It also has a custom user_details table. The JPA class is implementing UserDetails. I've also provided an implementation for UserDetailsService which looks up the user in my custom table.
OAuth Configuration is quite forward:
AuthorizationServerConfiguration - where oauth is configured:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableAuthorizationServer
class AuthorizationServerConfiguration : AuthorizationServerConfigurerAdapter() {
#Autowired private lateinit var authenticationManager: AuthenticationManager
#Autowired private lateinit var dataSource: DataSource
#Autowired
#Qualifier("customUserDetailsService")
internal lateinit var userDetailsService: UserDetailsService
#Autowired
private lateinit var passwordEncoder: BCryptPasswordEncoder
override fun configure(endpoints: AuthorizationServerEndpointsConfigurer) {
endpoints
.tokenStore(JdbcTokenStore(dataSource))
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
}
override fun configure(clients: ClientDetailsServiceConfigurer) {
// This one is used in conjunction with oauth_client_details. So like there's one app client and a few backend clients.
clients.jdbc(dataSource)
}
override fun configure(oauthServer: AuthorizationServerSecurityConfigurer) {
oauthServer.passwordEncoder(passwordEncoder)
}
}
WebSecurityConfiguration - needed for class above:
#Configuration
class WebSecurityConfiguration : WebSecurityConfigurerAdapter() {
#Bean // We need this as a Bean. Otherwise the entire OAuth service won't work.
override fun authenticationManagerBean(): AuthenticationManager {
return super.authenticationManagerBean()
}
override fun configure(http: HttpSecurity) {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
}
ResourceServerConfiguration - to configure access for endpoints:
#Configuration
#EnableResourceServer
class ResourceServerConfiguration : ResourceServerConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().cors().disable().csrf().disable()
.authorizeRequests()
.antMatchers("/oauth/token").authenticated()
.antMatchers("/oauth/user/**").authenticated()
.antMatchers("/oauth/custom_end_points/**").hasAuthority("my-authority")
// Deny everything else.
.anyRequest().denyAll()
}
}
These few lines give me a lot.
User Info endpoint (used by microservices)
Client's such as Mobile frontends can authenticate using: POST oauth/token and providing a grant_type=password together with a username and a password.
Servers can authorize using 'oauth/authorize'
Basic Auth support with different authorities is also available as I can fill username + password into the oauth_client_details table:
select client_id, access_token_validity, authorities, authorized_grant_types, refresh_token_validity, scope from oauth_client_details;
client_id | access_token_validity | authorities | authorized_grant_types | refresh_token_validity | scope
-------------------+-----------------------+-------------------------------+-------------------------------------------+------------------------+---------
backend | 864000 | mail,push,app-register | mail,push,client_credentials | 864000 | backend
app | 864000 | grant | client_credentials,password,refresh_token | 0 | app
This is used by the app if there's no oauth token yet.
Other microservices also use this to protect their endpoints - such as in this example:
#Configuration #EnableResourceServer class ResourceServerConfig : ResourceServerConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.authorizeRequests()
// Coach.
.antMatchers("/api/my-api/**").hasRole("my-role")
.antMatchers("/registration/**").hasAuthority("my-authority")
}
}
Their set up is quite easy:
security.oauth2.client.accessTokenUri=http://localhost:20200/oauth/token
security.oauth2.client.userAuthorizationUri=http://localhost:20200/oauth/authorize
security.oauth2.resource.userInfoUri=http://localhost:20200/oauth/user/me
security.oauth2.client.clientId=coach_client
security.oauth2.client.clientSecret=coach_client
The first three properties just go to my authorization server. The last two properties are the actual username + password that I've also inserted inside the oauth_client_details table. When my microservice wants to talk to another microservice it uses:
val details = ClientCredentialsResourceDetails()
details.clientId = "" // Values from the properties file.
details.clientSecret = "" // Values from the properties file.
details.accessTokenUri = "" // Values from the properties file.
val template = OAuth2RestTemplate(details)
template.exchange(...)
Now my question is - how can I get all of this with the built in Support from Spring Security using Spring Boot? I'd like to migrate away from the deprecated packages and retain all tokens so that users are still logged in afterwards.
We are also running a spring security authorization server and looked into this. Right now there is no replacement for the authorization server component in spring and there does not seem to be a timeline to implement one. Your best option would be to look into an existing auth component like keycloak or nimbus. alternatively there are hosted service like okta or auth0.
Keeping your existing tokens will be a bit of a challange as you would need to import them into your new solution. Our current tokens are opaque while newer auth-solutions tend to use some version of jwt, so depending on your tokens, keeping them may not even be an option.
Right now we consider accepting both old and new tokens for a time until the livetime of our old tokens ends, at wich point we would move fully to the new infrastukture.
So I've ended up developing my own authentication system with a migration API from the old Spring Security OAuth 2 to my system. That way you are not logged out and need to re-login.
I'll describe how I did it in case anyone is interested.
In my scenario it is 2 'microservices'. One being the deprecated auth and the other leveraging it.
Legacy Authentication System
To either get a token as a user you'd send a request to /oauth/token with your username + password.
To refresh a token another request to /oauth/token with your refresh token.
Both cases return your access token + refresh token. You can execute this multiple times per devices and you'd always end up with the same tokens. This is important later.
Tokens are stored as MD5 hashed.
Spring Security OAuth has these tables defined:
oauth_access_token (access tokens)
oauth_approvals (don't know what for, is always empty in my case)
oauth_client_details (contains a basic authorization method when you're not authorized)
oauth_client_token (empty in my case)
oauth_code (empty in my case)
oauth_refresh_token (refresh tokens)
user_details (contains the user data)
user_details_user_role (association between user + roles)
user_role (your roles)
I really didn't use the multi roles functionality, but in any case it's trivial to take that into consideration as well.
New Authentication System
Access token & refresh tokens are uuid4's that I SHA256 into my table.
I can query them easily and check for expiration and throw appropriate HTTP status codes.
I ended up doing a per device (it's just a UUID generated once in the frontend) system. That way I can distinguish when a user has multiple devices (AFAIK, this isn't possible with the old system).
We need these new endpoints
Login with email + password to get an authentication
Migration call from the old tokens to your new ones
Logout call which deletes your authentication
Refresh access token call
Thoughts
I can keep using the user_details table since only my code interacted with it and I expose it via Springs UserDetailsService.
I'll create a new authentication table that has a n:1 relationship to user_details where I store a device id, access token, access token expiry & refresh token per user.
To migrate from the old to the new system, my frontend will send a one time migration request, where I check for the given access token if it's valid and if it is, I generate new tokens in my system.
I'll handle both systems in parallel by distinguishing at the header level Authorization: Bearer ... for the old system & Authorization: Token ... for the new system
Code snippets
I use Kotlin, so in order to have type safety and not accidentally mix up my old / new token I ended up using a sealed inline classes:
sealed interface AccessToken
/** The token from the old mechanism. */
#JvmInline value class BearerAccessToken(val hashed: String) : AccessToken
/** The token from the new mechanism. */
#JvmInline value class TokenAccessToken(val hashed: String) : AccessToken
To get my token from an Authorization header String:
private fun getAccessToken(authorization: String?, language: Language) = when {
authorization?.startsWith("Bearer ") == true -> BearerAccessToken(hashed = hashTokenOld(authorization.removePrefix("Bearer ")))
authorization?.startsWith("Token ") == true -> TokenAccessToken(hashed = hashTokenNew(authorization.removePrefix("Token ")))
else -> throw BackendException(Status.UNAUTHORIZED, language.errorUnauthorized())
}
internal fun hashTokenOld(token: String) = MessageDigest.getInstance("MD5").digest(token.toByteArray(Charsets.UTF_8)).hex()
internal fun hashTokenNew(token: String) = MessageDigest.getInstance("SHA-256").digest(token.toByteArray(Charsets.UTF_8)).hex()
Verifying the tokens with type safety gets pretty easy:
when (accessToken) {
is BearerAccessToken -> validateViaDeprecatedAuthServer(role)
is TokenAccessToken -> {
// Query your table for the given accessToken = accessToken.hashed
// Ensure it's still valid and exists. Otherwise throw appropriate Status Code like Unauthorized.
// From your authentication table you can then also get the user id and work with your current user & return it from this method.
}
}
The validateViaDeprecatedAuthServer is using the old authentication sytem via the Spring APIs and returns the user id:
fun validateViaDeprecatedAuthServer(): String {
val principal = SecurityContextHolder.getContext().authentication as OAuth2Authentication
requireElseUnauthorized(principal.authorities.map { it.authority }.contains("YOUR_ROLE_NAME"))
return (principal.principal as Map<*, *>)["id"] as? String ?: throw IllegalArgumentException("Cant find id in principal")
}
Now we can verify if a given access token from a frontend is valid. The endpoint which generates a new token from the old one is also quite simple:
fun migrateAuthentication(accessToken: AccessToken) when (origin.accessToken(language)) {
is BearerAccessToken -> {
val userId = validateViaDeprecatedAuthServer(role)
// Now, create that new authentication in your new system and return it.
createAuthenticationFor()
}
is TokenAccessToken -> error("You're already migrated")
}
Creating authentication in your new system might look like this:
fun createAuthenticationFor() {
val refreshToken = UUID.randomUUID().toString()
val accessToken = UUID.randomUUID().toString()
// SHA256 both of them and save them into your table.
return refreshToken to accessToken
}
Then you only need some glue for your new 'login' endpoint where you need to check that the email / password matches a given user in your table, create an authentication & return it.
Logout just deletes the given authentication for your user id + device id.
Afterthoughts
I've been using this system now for the last few days and so far it's working nicely. Users are migrating. No one seems to be logged out which is exactly what I've wanted.
One downside is that since the old authentication system didn't distinguish between devices, I have no way of knowing when a user has successfully migrated. He could be using 1 device or 10. I simply don't know. So both systems will need to live side by side for a rather long time and slowly I'll phase out the old system. In which case, I'll force logout you and you need to re-login (and potentially install a new App version if you haven't updated).
Note that the new system is limited to my own needs, which is exactly what I want. I'd prefer it to be simple and maintainable than the Spring Blackbox authentication system.

Cookies path with Spring Cloud Gateway

Consider this microservices based application using Spring Boot 2.1.2 and Spring Cloud Greenwich.RELEASE:
Each microservice uses the JSESSIONID cookie to identify its own dedicated Servlet session (i.e. no global unique session shared with Spring Session and Redis).
External incoming requests are routed by Spring Cloud Gateway (and an Eureka registry used through Spring Cloud Netflix, but this should not be relevant).
When Spring Cloud Gateway returns a microservice response, it returns the "Set-Cookie" as-is, i.e. with the same "/" path.
When a second microservice is called by a client, the JSESSIONID from the first microservice is forwarded but ignored (since the corresponding session only exists in the first microservice). So the second microservice will return a new JSESSIONID. As a consequence the first session is lost.
In summary, each call to a different microservice will loose the previous session.
I expected some cookies path translation with Spring Cloud Gateway, but found no such feature in the docs. Not luck either with Google.
How can we fix this (a configuration parameter I could have missed, an
API to write such cookies path translation, etc)?
Rather than changing the JSESSIONID cookies path in a GlobalFilter, I simply changed the name of the cookie in the application.yml:
# Each microservice uses its own session cookie name to prevent conflicts
server.servlet.session.cookie.name: JSESSIONID_${spring.application.name}
I faced the same problem and found the following solution using Spring Boot 2.5.4 and Spring Cloud Gateway 2020.0.3:
To be independent from the Cookie naming of the downstream services, I decided to rename all cookies on the way through the gateway. But to avoid a duplicate session cookie in downstream requests (from the gateway itself) I also renamed the gateway cookie.
Rename the Gateway Session Cookie
Unfortunately customizing the gateway cookie name using server.servlet.session.cookie.name does not work using current gateway versions.
Therefore register a custom WebSessionManager bean (name required as the auto configurations is conditional on the bean name!) changing the cookie name (use whatever you like except typical session cookie names like SESSION, JSESSION_ID, …):
static final String SESSION_COOKIE_NAME = "GATEWAY_SESSION";
#Bean(name = WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME)
WebSessionManager webSessionManager(WebFluxProperties webFluxProperties) {
DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
CookieWebSessionIdResolver webSessionIdResolver = new CookieWebSessionIdResolver();
webSessionIdResolver.setCookieName(SESSION_COOKIE_NAME);
webSessionIdResolver.addCookieInitializer((cookie) -> cookie
.sameSite(webFluxProperties.getSession().getCookie().getSameSite().attribute()));
webSessionManager.setSessionIdResolver(webSessionIdResolver);
return webSessionManager;
}
Rename Cookies created
Next step is to rename (all) cookies set by the downstream server. This is easy as there is a RewriteResponseHeader filter available. I decided to simply add a prefix to every cookie name (choose a unique one for each downstream):
filters:
- "RewriteResponseHeader=Set-Cookie, ^([^=]+)=, DS1_$1="
Rename Cookies sent
Last step is to rename the cookies before sending to the downstream server. As every cookie of the downstream server has a unique prefix, just remove the prefix:
filters:
- "RewriteRequestHeader=Cookie, ^DS1_([^=]+)=, $1="
Arg, currently there is no such filter available. But based on the existing RewriteResponseHeader filter this is easy (the Cloud Gateway will use it if you register it as a bean):
#Component
class RewriteRequestHeaderGatewayFilterFactory extends RewriteResponseHeaderGatewayFilterFactory
{
#Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest().mutate()
.headers(httpHeaders -> rewriteHeaders(httpHeaders, config)).build();
return chain.filter(exchange.mutate().request(request).build());
}
#Override
public String toString() {
return filterToStringCreator(RewriteRequestHeaderGatewayFilterFactory.this)
.append("name", config.getName()).append("regexp", config.getRegexp())
.append("replacement", config.getReplacement()).toString();
}
};
}
private void rewriteHeaders(HttpHeaders httpHeaders, Config config)
{
httpHeaders.put(config.getName(), rewriteHeaders(config, httpHeaders.get(config.getName())));
}
}
Simply reset cookie name to GATEWAY_SESSION in gateway project to avoid session conflict:
#Autowired(required = false)
public void setCookieName(HttpHandler httpHandler) {
if (httpHandler == null) return;
if (!(httpHandler instanceof HttpWebHandlerAdapter)) return;
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
CookieWebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver();
sessionIdResolver.setCookieName("GATEWAY_SESSION");
sessionManager.setSessionIdResolver(sessionIdResolver);
((HttpWebHandlerAdapter) httpHandler).setSessionManager(sessionManager);
}

Invalid JWToken: kid is a required JOSE Header

I am trying to implement an Oauth2 Authorization Server with SpringBoot using this guide as a reference.
My keystore has a single key. I have successfully managed to create a JWToken (I can check it at jwt.io).
I have also a test Resource Server. When I try to access any endpoint I receive the following message:
{
"error": "invalid_token",
"error_description": "Invalid JWT/JWS: kid is a required JOSE Header"
}
The token really does not have a kid header but I can not figure out how to add it. I can only add data to its payload, using a TokenEnchancer. It also seems that I am not the first one with this issue.
Is there any way to add this header or, at least, ignore it at the resource server?
I've been working on an article that might help you out here:
https://www.baeldung.com/spring-security-oauth2-jws-jwk
So, to configure a Spring Security OAuth Authorization Server to add a JWT kid header, you can follow the steps of section 4.9:
create a new class extending the JwtAccessTokenConverter
In the constructor:
configure the parent class using the same approach you've been using
obtain a Signer object using the signing key you're using
override the encode method. The implementation will be the same as the parent one, with the only difference that you’ll also pass the custom headers when creating the String token
public class JwtCustomHeadersAccessTokenConverter extends JwtAccessTokenConverter {
private JsonParser objectMapper = JsonParserFactory.create();
final RsaSigner signer;
public JwtCustomHeadersAccessTokenConverter(KeyPair keyPair) {
super();
super.setKeyPair(keyPair);
this.signer = new RsaSigner((RSAPrivateKey) keyPair.getPrivate());
}
#Override
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String content;
try {
content = this.objectMapper.formatMap(getAccessTokenConverter().convertAccessToken(accessToken, authentication));
} catch (Exception ex) {
throw new IllegalStateException("Cannot convert access token to JSON", ex);
}
Map<String, String> customHeaders = Collections.singletonMap("kid", "my_kid");
String token = JwtHelper.encode(content, this.signer, this.customHeaders)
.getEncoded();
return token;
}
}
Then, of course, create a bean using this converter:
#Bean
public JwtAccessTokenConverter accessTokenConverter(KeyPair keyPair) {
return new JwtCustomHeadersAccessTokenConverter(keyPair);
}
Here I used a KeyPair instance to obtain the signing key and configure the converter (based on the example of the article), but you might adapt that to your configuration.
In the article I also explain the relevant endpoints provided by the Spring Security OAuth Authentication Server.
Also, regarding #Ortomala Lokni's comment, I wouldn't expect Spring Security OAuth to add any new features at this point. As an alternative, you probably can wait to have a look at Spring Security's Authorization Server features, planned to be released in 5.3.0
I managed to solve it by changing the parameter used to identify the URL where the clients will retrieve the pubkey.
On application.properties, instead of:
security.oauth2.resource.jwk.key-set-uri=http://{auth_server}/.well-known/jwks.json
I used:
security.oauth2.resource.jwt.key-uri=http://{auth_server}/oauth/token_key
If I understood correctly, the key-set-uri config points to an endpoint that presents a set of keys and there is the need for a kid. On the other side key-uri config points to an endpoint with a single key.

Spring Security Custom Authentication Filter and Authorization

I've implemented a custom authentication filter, and it works great. I use an external identity provider and redirect to my originally requested URL after setting my session and adding my authentication object to my security context.
Security Config
#EnableWebSecurity(debug = true)
#Configuration
class SecurityConfig extends WebSecurityConfigurerAdapter {
// this is needed to pass the authentication manager into our custom security filter
#Bean
#Override
AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean()
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
//.antMatchers("/admin/test").hasRole("METADATA_CURATORZ")
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new CustomSecurityFilter(authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)
}
}
Filter logic
For now, my custom filter (once identity is confirmed) simply hard codes a role:
SimpleGrantedAuthority myrole = new SimpleGrantedAuthority("METADATA_CURATORZ")
return new PreAuthenticatedAuthenticationToken(securityUser, null, [myrole])
That authentication object (returned above) is then added to my SecurityContext before redirecting to the desired endpoint:
SecurityContextHolder.getContext().setAuthentication(authentication)
Controller Endpoint
#RequestMapping(path = '/admin/test', method = GET, produces = 'text/plain')
String test(HttpServletRequest request) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication()
String roles = auth.getAuthorities()
return "roles: ${roles}"
}
This endpoint then yields a response in the browser of:
"roles: [METADATA_CURATORZ]"
Great. So my authentication and applying a role to my user is working great.
Now, if I uncomment this line from the security config:
//.antMatchers("/admin/test").hasRole("METADATA_CURATORZ")
I can no longer access that resource and get a 403 -- even though we've already proven the role was set.
This seems totally nonsensical and broken to me, but I'm no Spring Security expert.
I'm probably missing something very simple. Any ideas?
Some questions I have:
Does my custom filter need to be placed before a specific built-in filter to ensure the authorization step occurs after that filter is executed?
When in the request cycle is the antMatcher/hasRole check taking place?
Do I need to change the order of what I am calling in my security configure chain, and how should I understand the config as I've currently written it? It's obviously not doing what I think it should be.
Does my custom filter need to be placed before a specific built-in filter to ensure the authorization step occurs after that filter is executed?
Your filter MUST come before FilterSecurityInterceptor, because that is where authorization and authentication take place. This filter is one of the last to be invoked.
Now as to where the best place for your filter might be, that really depends. For example, you really want your filter to come before AnonymousAuthenticationFilter because if not, unauthenticated users will always be "authenticated" with an AnonymousAuthenticationToken by the time your filter is invoked.
You can check out the default order of filters in FilterComparator. The AbstractPreAuthenticatedProcessingFilter pretty much corresponds to what it is you're doing - and its placement in the order of filters gives you an idea of where you could put yours. In any case, there should be no issue with your filter's order.
When in the request cycle is the antMatcher/hasRole check taking place?
All of this happens in FilterSecurityInterceptor, and more precisely, in its parent AbstractSecurityInterceptor:
protected InterceptorStatusToken beforeInvocation(Object object) {
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
...
}
...
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
...
throw accessDeniedException;
}
Extra information:
In essence, the FilterSecurityInterceptor has a ExpressionBasedFilterInvocationSecurityMetadataSource that contains a Map<RequestMatcher, Collection<ConfigAttribute>>. At runtime, your request is checked against the Map to see if any RequestMatcher key is a match. If it is, a Collection<ConfigAttribute> is passed to the AccessDecisionManager, which ultimately either grants or denies access. The default AccessDecisionManager is AffirmativeBased and contains objects (usually a WebExpressionVoter) that process the collection of ConfigAttribute and via reflection invokes the SpelExpression that corresponds to your "hasRole('METADATA_CURATORZ')" against a SecurityExpressionRoot object that was initialized with your Authentication.
Do I need to change the order of what I am calling in my security configure chain, and how should I understand the config as I've currently written it? It's obviously not doing what I think it should be.
No, there shouldn't be any issue with your filters. Just as a side note, in addition to what you have in your configure(HttpSecurity http) methods, the WebSecurityConfigurerAdapter you extend from has some defaults:
http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoginPageConfigurer<>()).and()
.logout();
You can take a look at HttpSecurity if you want to see exactly what these do and what filters they add.
THE PROBLEM
When you do the following:
.authorizeRequests()
.antMatchers("/admin/test").hasRole("METADATA_CURATORZ")
... the role that is searched for is "ROLE_METADATA_CURATORZ". Why?
ExpressionUrlAuthorizationConfigurer's static hasRole(String role) method ends up processing "METADATA_CURATORZ":
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException(
"role should not start with 'ROLE_' since it is automatically inserted. Got '"
+ role + "'");
}
return "hasRole('ROLE_" + role + "')";
}
So your authorization expression becomes "hasRole('ROLE_METADATA_CURATORZ'" and this ends up calling the method hasRole('ROLE_METADATA_CURATORZ') on SecurityExpressionRoot, which in turn searches for the role ROLE_METADATA_CURATORZ in the Authentication's authorities.
THE SOLUTION
Change
SimpleGrantedAuthority myrole = new SimpleGrantedAuthority("METADATA_CURATORZ");
to:
SimpleGrantedAuthority myrole = new SimpleGrantedAuthority("ROLE_METADATA_CURATORZ");

Resources