-
Notifications
You must be signed in to change notification settings - Fork 5.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Differentiate cases when using default SecurityExpression and Authori… #15412
Conversation
…zation header is missing
@patternknife Please sign the Contributor License Agreement! Click here to manually synchronize the status of this Pull Request. See the FAQ for frequently asked questions. |
@patternknife Thank you for signing the Contributor License Agreement! |
Hi, @patternknife, thanks for the PR. I don't understand, though, why this change is necessary. When an application throws an It sounds like something else may be going on. Can you produce a minimal GitHub sample that reproduces the issue you are seeing? |
Hi, @jzheaux , as the abstract class SecurityExpressionRoot, which implements SecurityExpressionOperations, only returns a boolean type:
Consequently, I can't distinguish whether the header is omitted or if the user doesn't have the proper roles.
You can check the OAuth2 Resource Server source code at "https://github.com/patternknife/spring-security-oauth2-password-jpa-implementation/blob/main/src/main/java/io/github/patternknife/securityhelper/oauth2/api/config/security/server/ServerConfig.java." (Here, KnifeOauth2AuthenticationException is a custom exception that I created, which implements OAuth2AuthenticationException in 'Spring Authorization Server'. ) package com.patternknife.securityhelper.oauth2.client.domain.common.api;
@RestController
public class BaseApi {
@Value("${spring.profiles.active}")
private String activeProfile;
// AccessDeniedException
@PreAuthorize("isAuthenticated()")
@GetMapping("/systemProfile")
public String checkAuthenticated () {
return activeProfile;
}
// AccessDeniedException
@PreAuthorize("@customAuthorityService.hasAnyUserRole()")
@GetMapping("/systemProfile2")
public String checkPermissions () {
return activeProfile;
}
} package com.patternknife.securityhelper.oauth2.client.config.securityimpl.serivce.permission;
@Service
public class CustomAuthorityService {
public boolean hasAnyUserRole() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication == null){
throw new KnifeOauth2AuthenticationException();
}
if (authentication.getAuthorities() == null) {
return false;
}
return authentication.getAuthorities().stream()
.anyMatch(authority -> authority.getAuthority().endsWith("_USER"));
}
}
```java
package io.github.patternknife.securityhelper.oauth2.api.config.security.server;
@Bean
@Order(2)
public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http, OAuth2AuthorizationServiceImpl authorizationService,
ConditionalDetailsService conditionalDetailsService,
ISecurityUserExceptionMessageService iSecurityUserExceptionMessageService,
AuthenticationEntryPoint iAuthenticationEntryPoint
) throws Exception {
DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
resolver.setAllowFormEncodedBodyParameter(true);
http.csrf(AbstractHttpConfigurer::disable)
.oauth2ResourceServer(oauth2 -> oauth2
.bearerTokenResolver(resolver)
.authenticationEntryPoint(iAuthenticationEntryPoint)
.opaqueToken(opaqueToken -> opaqueToken.introspector(tokenIntrospector(authorizationService, conditionalDetailsService, iSecurityUserExceptionMessageService))));
return http.build();
} |
I agree that if you don't have an authorization header, you don't know if the principal has the appropriate roles. However, it's also true that if you don't have an authorization header, then you don't know who the principal is at all. This makes it a concern for authentication, not authorization. IOW, you already have enough to differentiate between a 401 and a 403. 401 means either no credentials were provided or the credentials are bad This is how Spring Security works by default. Is there a reason you need to deviate from that? |
Well, if I inject a customized function into @PreAuthorize, I can distinguish the two cases.
However, by default, is the case of the omitted Authorization header in a normal OAuth2 setup (not only in Spring Security) regarded as an AuthenticationException (401), not an AccessDeniedException (403)? I think the expressions "isAuthenticated" and "isFullyAuthenticated" should throw a 401 by default, but ThrowingMethodAuthorizationDeniedHandler only throws a 403. If someone uses the default (@PreAuthorize("isAuthenticated()")) and omits the header, they will face a 403. The committed code should also face a 403, but with different messages, as the class was restricted to throwing only an AccessDeniedException. I don't know, but I was just wondering if the default omitted Authorization header case needed to be handled as a 401 by default or be with a different message. |
Thanks for the extra info, @patternknife.
This shouldn't be necessary as the framework does this for you. When Maybe I've made an assumption. Are you getting responses from your application that don't align with the following expectations:
If so, please try this: @Service
public class CustomAuthorityService {
public boolean hasAnyUserRole() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication == null){
throw new AccessDeniedException("access is denied");
}
if (authentication.getAuthorities() == null) {
return false;
}
return authentication.getAuthorities().stream()
.anyMatch(authority -> authority.getAuthority().endsWith("_USER"));
}
}
If that doesn't address your concern, please respond with a GitHub sample project that demonstrates what appears to be broken. |
"AuthenticationEntryPoint" here seems to catch only an https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html
So the following throws an
Your code doesn't seem to be caught by the "AuthenticationEntryPoint" in the ExceptionTranslationFilter, as it throws an |
Gotcha. Yes, Does that help? If not, please provide a sample and I'll see if I can better understand what changes are needed if any. |
I see. But this code is customized.
The following default class doesn't send the missing authorization header case to AuthenticationEntryPoint.
I think the case of a missing Authorization header should be handled as an AuthenticationException sent to an AuthenticationEntryPoint, same to the case with an incorrect Authorization value, but not at this point. |
I think at this point I'm going to need a sample. Would you post something to GitHub that demonstrates the request/response behavior you would like and what you are needing to do to achieve that? I'd be happy to push a PR to that repo if a more idiomatic arrangement is possible. |
@patternknife, I'm sorry that we weren't able to get aligned on this PR. At this point, I'm going to close it. If you'd like to continue collaborating, please post a question to StackOverflow and paste the link here. |
When the Authorization header is missing, the process does not proceed to AuthenticationProvider, which is supposed to result in a 401 error rather than a 403 error due to insufficient permissions.
As it does not seem to be appropriate to directly throw an AuthenticationException from ThrowingMethodAuthorizationDeniedHandler, that codes only allows developers to distinguish and handle such cases in @ExceptionHandler.
This issue was discovered during testing by calling oauth2/token with spring-security-oauth2-authorization-server(1.2.3).