-
Notifications
You must be signed in to change notification settings - Fork 4.1k
OAuth2 client flow with authorization code grant is not possible with anonymous auth #1842
Comments
Thanks for getting in touch, but it feels like this is a question that would be better suited to Stack Overflow. As mentioned in the guidelines for contributing, we prefer to use GitHub issues only for bugs and enhancements. Feel free to update this issue with a link to the re-posted question (so that other people can find it) or add some more details if you feel this is a genuine bug. |
Yes, I guess you could call this a bug - OAuth2 flow with authorization code grant is not working with anonymous auth. But, that's just a matter of phrasing. I wrote the issue report this way, because I'd like to settle on a workaround, since waiting for a new bugfix release and upgrading is not an option for me. |
This is not a valid use case. The |
Respectfully sir, it is a valid use case. This is perfectly doable in Spring Security 5.2.x. Each anonymous authentication has a separate HTTP session, and OAuth2 Authorized clients are bound to the specific session. There is no need to have the user authenticated in a Spring client application (third-party in the spec). The OAuth2 standard does not require it - in fact the third-party's access management is out of scope of the OAuth2 spec. |
@mdindoffer Thanks for clarifying your issue further. Yes, you are correct, the user does not need to be authenticated with the client app, however, does need to be authenticated with the provider. As the next step in helping you troubleshoot this issue, it would be helpful if you could provide a minimal sample that reproduces the issue. Then I could look at your sample and advise on a solution. |
@jgrandja NP, I have created a tiny Springboot client app with OAuth credentials for testing provided. If you'd prefer to read through the code here, I will paste the contents below: AuthController.javapackage eu.dindoffer.example.oauth.client.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@Controller
public class AuthController {
private static final Logger LOG = LoggerFactory.getLogger(AuthController.class);
private OAuth2RestOperations exampleProviderRestTemplate;
public AuthController(OAuth2RestOperations exampleProviderRestTemplate) {
this.exampleProviderRestTemplate = exampleProviderRestTemplate;
}
@GetMapping("/initNewAuth")
public void initNewAuth(HttpServletRequest request, HttpServletResponse response) throws IOException {
LOG.info("Initiating new auth request");
//clear the session
clearCurrentAuthentication(request);
//Create a new session and store any data if needed (omitted for brevity)
HttpSession session = request.getSession();
//Redirect to initiate OAuth flow with an example provider
response.sendRedirect("/oauth2/authorization/example-provider");
}
@GetMapping(value = "/authorize/oauth2/code/{providerId}", params = "!error")
public void handleSuccessfulAuth(@PathVariable String providerId) {
LOG.info("OAuth grant successful for provider {}", providerId);
//try to obtain access token
OAuth2AccessToken accessToken = exampleProviderRestTemplate.getAccessToken();
LOG.info("Obtained access token: {}", accessToken);
//Now do stuff, e.g. try to call protected resources with an access token...
}
@GetMapping(value = "/authorize/oauth2/code/{providerId}", params = "error")
public void handleFailedAuth(@PathVariable("providerId") String providerId,
@RequestParam("error") String error,
@RequestParam(value = "error_description", required = false) String errorDescription,
@RequestParam(value = "error_uri", required = false) String errorUri) {
LOG.error("Caught an unsuccessful auth for provider {}, with error {}", providerId, error);
}
/**
* Performs a manual logout by removing any current authentication present.
* Invalidates the session of the provided request, removes the Authentication
* object from security context and detaches the security context from the current thread.
*
* @param request current HTTP request
*/
private void clearCurrentAuthentication(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
LOG.debug("Invalidating session:{}", session.getId());
session.invalidate();
}
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(null);
SecurityContextHolder.clearContext();
}
} application.ymllogging:
level:
org.springframework:
security: DEBUG
eu.dindoffer.example.oauth.client:
oauth:
example-registration:
clientId: b7d43f9b-a78a-4907-a6d2-a5e94c15dbc0
clientSecret: 22f7c0f6-0218-4fd9-a4b7-500faaa2b2da
accessTokenUri: https://webapi.developers.erstegroup.com/api/csas/sandbox/v1/sandbox-idp/token
userAuthorizationUri: https://webapi.developers.erstegroup.com/api/csas/sandbox/v1/sandbox-idp/auth
authenticationScheme: header
clientAuthenticationScheme: form
preEstablishedRedirectUri: http://localhost:8080/authorize/oauth2/code/example
use-current-uri: false
scope: kyc AppConfig.javapackage eu.dindoffer.example.oauth.client.config;
import eu.dindoffer.example.oauth.client.impl.MyAccessTokenProviderChain;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter;
import org.springframework.security.oauth2.client.token.AccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.web.filter.CompositeFilter;
import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Configuration
@EnableOAuth2Client
public class AppConfig extends WebSecurityConfigurerAdapter {
@Autowired
private OAuth2ClientContext oauth2ClientContext;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().permitAll();
}
@Bean
public OAuth2RestOperations exampleProviderRestTemplate(OAuth2ClientContext oauth2ClientContext) {
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(exampleRegistrationResourceDetails(), oauth2ClientContext);
//TODO Uncomment this for an attempted workaround by avoiding anonymous auth checks
// MyAccessTokenProviderChain myAccessTokenProviderChain = new MyAccessTokenProviderChain(Collections.<AccessTokenProvider>singletonList(
// new AuthorizationCodeAccessTokenProvider()
// ));
// oAuth2RestTemplate.setAccessTokenProvider(myAccessTokenProviderChain);
return oAuth2RestTemplate;
}
@Bean
@ConfigurationProperties("eu.dindoffer.example.oauth.client.oauth.example-registration")
public AuthorizationCodeResourceDetails exampleRegistrationResourceDetails() {
return new AuthorizationCodeResourceDetails();
}
@Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
@Bean
public FilterRegistrationBean oauthLoginFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(oauth2LoginFilter());
registration.setOrder(-100);
return registration;
}
private Filter oauth2LoginFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
// add a new filter dedicated to an example OAuth2 provider
OAuth2ClientAuthenticationProcessingFilter exampleOauthProviderFilter =
new OAuth2ClientAuthenticationProcessingFilter("/oauth2/authorization/example-provider");
OAuth2RestOperations exampleProviderRestTemplate = exampleProviderRestTemplate(oauth2ClientContext);
exampleOauthProviderFilter.setRestTemplate(exampleProviderRestTemplate);
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(new InMemoryTokenStore());
exampleOauthProviderFilter.setTokenServices(defaultTokenServices);
filters.add(exampleOauthProviderFilter);
// add filters for additional providers here...
filter.setFilters(filters);
return filter;
}
} MyAccessTokenProviderChain.javapackage eu.dindoffer.example.oauth.client.impl;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
import org.springframework.security.oauth2.client.token.AccessTokenProvider;
import org.springframework.security.oauth2.client.token.AccessTokenProviderChain;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import java.util.List;
public class MyAccessTokenProviderChain extends AccessTokenProviderChain {
public MyAccessTokenProviderChain(List<? extends AccessTokenProvider> chain) {
super(chain);
}
/**
* Basically the same implementation as parent, sans the:
* 1. Anonymous auth check
* 2. Interaction with the ClientTokenServices
*/
@Override
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails resource, AccessTokenRequest request) throws UserRedirectRequiredException, AccessDeniedException {
OAuth2AccessToken accessToken = null;
OAuth2AccessToken existingToken = null;
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (resource.isClientOnly() || (auth != null && auth.isAuthenticated())) {
existingToken = request.getExistingToken();
if (existingToken != null) {
if (existingToken.isExpired()) {
OAuth2RefreshToken refreshToken = existingToken.getRefreshToken();
if (refreshToken != null) {
accessToken = refreshAccessToken(resource, refreshToken, request);
}
} else {
accessToken = existingToken;
}
}
}
// Give unauthenticated users a chance to get a token and be redirected
if (accessToken == null) {
// looks like we need to try to obtain a new token.
accessToken = obtainNewAccessTokenInternal(resource, request);
if (accessToken == null) {
throw new IllegalStateException(
"An OAuth 2 access token must be obtained or an exception thrown.");
}
}
return accessToken;
}
} |
@mdindoffer I took a look at the sample and have a couple of things to point out. First, if your application acts as a client only, then no need to register Second, the workaround for An alternative workaround is registering a custom |
Thank you very much for the advice! As far as I'm concerned, this issue can be closed, as a reasonable workaround exists, but I'll leave the decision to you. |
@mdindoffer I'm sorry to hear you moved off of Spring Security OAuth. Is there a reason you moved to ScribeJava instead of Spring Security's 5.x OAuth 2.0 support? I'm more curious than anything as feedback is always welcome. Also, in case you are not aware, we just started the new project Spring Authorization Server. Either way, I'll close this issue since a workaround has been provided. |
Well, I currently work as a contractor for a large company. For reasons unknown, they have built their own "framework" for microservices on top of SpringBoot and Spring Cloud that is forced upon every project. I'm working on a new microservice in an older project built with an older version of the proprietary framework (with SpringBoot 1.5.13) and was told specifically to not upgrade. I guess because of compatibility or maintenance issues? The "microservices" share a common maven parent with common dependency management TLDR: There's no technical reason why, other than bad architectural and management decisions. Spring Security 5 has good enough support for OAuth2. |
Thanks for the feedback @mdindoffer ! |
Disclaimer
I know this library is deprecated, but I was forced to migrate to an older Springboot stack.
Summary
The OAuth2 client authorization flow with authorization code grant type does not work together with anonymous authentication.
Is a hack/workaround available?
Actual Behavior
I need to allow anonymous users (with anonymous HTTP session / authentication) to access remote OAuth2 resources protected by authorization code grant.
I can successfully redirect users to the remote Authorization/IDP login page, but get exceptions everytime I'm trying to complete the auth flow.
There are two issues I face:
OAuth2ClientAuthenticationProcessingFilter
, the filter properly requests and obtains an access token. However, the next thing it tries to do is:Which fails on
BadCredentialsException("Could not obtain user details from token")
, because theDefaultTokenService
tries to calltokenStore#readAccessToken
, but theInMemoryTokenStore
is entirely empty at this point.OAuth2ClientAuthenticationProcessingFilter
I cannot even obtain the access token. When trying to callrestTemplate.getAccessToken()
in a RestController, the call fails inAccessTokenProviderChain
due to this exception:I've tried to use my own implementation of
AccessTokenProviderChain
, that simply omits this check, and at first look it seems to work, but I am not sure if this is a correct approach sufficient for multitenancy.Expected Behavior
I expect there would be no checks in regards to anonymous authentication. Why should it matter if we have some user details or not? All I want to do is to call some protected resource on behalf of an anonymous user, that provided consent at the Authorization Server.
This is perfectly plausible and doable in Spring Security 5.2.x, because I've already done so, but I need to port the solution to spring security oauth2.
Why does the AccessTokenProviderChain check for anonymous auths?
What do I need to override and reimplement for this scenario to work?
Configuration
I'm using simple
@EnableOAuth2Client
,DefaultTokenServices
(since I don't want to fetch user data from any remote service),InMemoryTokenStore
(since that's the simplest one available out of the box).Version
Springboot 1.5.13, Spring Security OAuth 2.0.15.RELEASE.
The text was updated successfully, but these errors were encountered: