Reputation: 3129
I am trying to build a Spring MVC application and securing it with Spring Security OAuth2 and the provider is Google. I was able to get the web app working without security and with form login. However, I am not able to get OAuth with google to work. Google app setup is fine as I can get the call backs etc to work with a non Spring Security app.
My security config is as follows:
<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns:sec="http://www.springframework.org/schema/security"
xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
<sec:http use-expressions="true" entry-point-ref="clientAuthenticationEntryPoint">
<sec:http-basic/>
<sec:logout/>
<sec:anonymous enabled="false"/>
<sec:intercept-url pattern="/**" access="isFullyAuthenticated()"/>
<sec:custom-filter ref="oauth2ClientContextFilter" after="EXCEPTION_TRANSLATION_FILTER"/>
<sec:custom-filter ref="googleAuthenticationFilter" before="FILTER_SECURITY_INTERCEPTOR"/>
</sec:http>
<b:bean id="clientAuthenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint"/>
<sec:authentication-manager alias="alternateAuthenticationManager">
<sec:authentication-provider>
<sec:user-service>
<sec:user name="user" password="password" authorities="DOMAIN_USER"/>
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
</b:beans>
The OAuth2 protected resource is as follows:
@Configuration
@EnableOAuth2Client
class ResourceConfiguration {
@Autowired
private Environment env;
@Resource
@Qualifier("accessTokenRequest")
private AccessTokenRequest accessTokenRequest;
@Bean
public OAuth2ProtectedResourceDetails googleResource() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setId("google-app");
details.setClientId(env.getProperty("google.client.id"));
details.setClientSecret(env.getProperty("google.client.secret"));
details.setAccessTokenUri(env.getProperty("google.accessTokenUri"));
details.setUserAuthorizationUri(env.getProperty("google.userAuthorizationUri"));
details.setTokenName(env.getProperty("google.authorization.code"));
String commaSeparatedScopes = env.getProperty("google.auth.scope");
details.setScope(parseScopes(commaSeparatedScopes));
details.setPreEstablishedRedirectUri(env.getProperty("google.preestablished.redirect.url"));
details.setUseCurrentUri(false);
details.setAuthenticationScheme(AuthenticationScheme.query);
details.setClientAuthenticationScheme(AuthenticationScheme.form);
return details;
}
private List<String> parseScopes(String commaSeparatedScopes) {
List<String> scopes = newArrayList();
Collections.addAll(scopes, commaSeparatedScopes.split(","));
return scopes;
}
@Bean
public OAuth2RestTemplate googleRestTemplate() {
return new OAuth2RestTemplate(googleResource(), new DefaultOAuth2ClientContext(accessTokenRequest));
}
@Bean
public AbstractAuthenticationProcessingFilter googleAuthenticationFilter() {
return new GoogleOAuthentication2Filter(new GoogleAppsDomainAuthenticationManager(), googleRestTemplate(), "https://accounts.google.com/o/oauth2/auth", "http://localhost:9000");
}
}
The custom authentication filter which I have written to throw a Redirect exception to get the OAuth2 authorization is as follows:
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
try {
logger.info("OAuth2 Filter Triggered!! for path {} {}", request.getRequestURI(), request.getRequestURL().toString());
logger.info("OAuth2 Filter hashCode {} request hashCode {}", this.hashCode(), request.hashCode());
String code = request.getParameter("code");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
logger.info("Code is {} and authentication is {}", code, authentication == null ? null : authentication.isAuthenticated());
// not authenticated
if (requiresRedirectForAuthentication(code)) {
URI authURI = new URI(googleAuthorizationUrl);
logger.info("Posting to {} to trigger auth redirect", authURI);
String url = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + oauth2RestTemplate.getAccessToken();
logger.info("Getting profile data from {}", url);
// Should throw RedirectRequiredException
oauth2RestTemplate.getForEntity(url, GoogleProfile.class);
// authentication in progress
return null;
} else {
logger.info("OAuth callback received");
// get user profile and prepare the authentication token object.
String url = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + oauth2RestTemplate.getAccessToken();
logger.info("Getting profile data from {}", url);
ResponseEntity<GoogleProfile> forEntity = oauth2RestTemplate.getForEntity(url, GoogleProfile.class);
GoogleProfile profile = forEntity.getBody();
CustomOAuth2AuthenticationToken authenticationToken = getOAuth2Token(profile.getEmail());
authenticationToken.setAuthenticated(false);
Authentication authenticate = getAuthenticationManager().authenticate(authenticationToken);
logger.info("Final authentication is {}", authenticate == null ? null : authenticate.isAuthenticated());
return authenticate;
}
} catch (URISyntaxException e) {
Throwables.propagate(e);
}
return null;
}
The filter chain sequence from the Spring web app is as follows:
o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'metricFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'oauth2ClientContextFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'googleOAuthFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'org.springframework.security.filterChainProxy' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'org.springframework.security.web.access.intercept.FilterSecurityInterceptor#0' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'applicationContextIdFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'webRequestLoggingFilter' to: [/*]
The redirect to Google works fine and I get the callback to the filter and the authentication is successful. However after that, the request results in a redirect and it invokes the filter again (the request is the same, I have checked the hasCode). On the second call the authentication in the SecurityContext
is null
. As part of the first authentication call the Authentication object was populated in the security context, so why does it disappear?
I am working with Spring Security for the first time so may have made newbie mistake.
Upvotes: 1
Views: 6611
Reputation: 1
Not to impose a better answer, but another reason for this redirect looping is the session cookie handling when the session cookie sameSite attribute is set to Strict. Then, if the authority service has broken the redirect chain, the session cookie will not be transmitted.
See: How can I redirect after OAUTH2 with SameSite=Strict and still get my cookies?
Upvotes: 0
Reputation: 3129
After playing around with Spring Security configuration and the filters I was finally able to get this working. I had to make couple of important changes
org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter
) instead of the custom filter I was using./googleLogin
and added an authentication entry point which redirects to this URL on authentication failure.Overall the flow is as follows
/
and the request passes through the OAuth2ClientContextFilter
and OAuth2ClientAuthenticationProcessingFilter
as the context does not match. The configured context path for login is /googleLogin
FilterSecurityInterceptor
detects that the the user is anonymous and throws an access denied exception.ExceptionTranslationFilter
catches the access denied exception and asks the configured authentication entry point to handle it which issues a redirect to /googleLogin
./googleLogin
, the filter OAuth2AuthenticationProcessingFilter
tries to access the Google protected resource and an UserRedirectRequiredException
is thrown which is translated into a HTTP redirect to Google (with the OAuth2 details) by OAuth2ClientContextFilter
./googleLogin
with the OAuth code. The filter OAuth2AuthenticationProcessingFilter
handles this and creates an Authentication
object and updates the SecurityContext
. OAuth2AuthenticationProcessingFilter
. FilterSecurityInterceptor
allows the request to proceed as the SecurityContext
contains an Authentication object
which is authenticated. isFullyAuthenticated()
or similar is rendered.The security context xml is as follows:
<sec:http use-expressions="true" entry-point-ref="clientAuthenticationEntryPoint">
<sec:http-basic/>
<sec:logout/>
<sec:anonymous enabled="false"/>
<sec:intercept-url pattern="/**" access="isFullyAuthenticated()"/>
<!-- This is the crucial part and the wiring is very important -->
<!--
The order in which these filters execute are very important. oauth2ClientContextFilter must be invoked before
oAuth2AuthenticationProcessingFilter, that's because when a redirect to Google is required, oAuth2AuthenticationProcessingFilter
throws a UserRedirectException which the oauth2ClientContextFilter handles and generates a redirect request to Google.
Subsequently the response from Google is handled by the oAuth2AuthenticationProcessingFilter to populate the
Authentication object and stored in the SecurityContext
-->
<sec:custom-filter ref="oauth2ClientContextFilter" after="EXCEPTION_TRANSLATION_FILTER"/>
<sec:custom-filter ref="oAuth2AuthenticationProcessingFilter" before="FILTER_SECURITY_INTERCEPTOR"/>
</sec:http>
<b:bean id="oAuth2AuthenticationProcessingFilter" class="org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter">
<b:constructor-arg name="defaultFilterProcessesUrl" value="/googleLogin"/>
<b:property name="restTemplate" ref="googleRestTemplate"/>
<b:property name="tokenServices" ref="tokenServices"/>
</b:bean>
<!--
These token classes are mostly a clone of the Spring classes but have the structure modified so that the response
from Google can be handled.
-->
<b:bean id="tokenServices" class="com.rst.oauth2.google.security.GoogleTokenServices">
<b:property name="checkTokenEndpointUrl" value="https://www.googleapis.com/oauth2/v1/tokeninfo"/>
<b:property name="clientId" value="${google.client.id}"/>
<b:property name="clientSecret" value="${google.client.secret}"/>
<b:property name="accessTokenConverter">
<b:bean class="com.rst.oauth2.google.security.GoogleAccessTokenConverter">
<b:property name="userTokenConverter">
<b:bean class="com.rst.oauth2.google.security.DefaultUserAuthenticationConverter"/>
</b:property>
</b:bean>
</b:property>
</b:bean>
<!--
This authentication entry point is used for all the unauthenticated or unauthorised sessions to be directed to the
/googleLogin URL which is then intercepted by the oAuth2AuthenticationProcessingFilter to trigger authentication from
Google.
-->
<b:bean id="clientAuthenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<b:property name="loginFormUrl" value="/googleLogin"/>
</b:bean>
Also the Java Config for the OAuth2 resources is as follows:
@Configuration
@EnableOAuth2Client
class OAuth2SecurityConfiguration {
@Autowired
private Environment env;
@Resource
@Qualifier("accessTokenRequest")
private AccessTokenRequest accessTokenRequest;
@Bean
@Scope("session")
public OAuth2ProtectedResourceDetails googleResource() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setId("google-oauth-client");
details.setClientId(env.getProperty("google.client.id"));
details.setClientSecret(env.getProperty("google.client.secret"));
details.setAccessTokenUri(env.getProperty("google.accessTokenUri"));
details.setUserAuthorizationUri(env.getProperty("google.userAuthorizationUri"));
details.setTokenName(env.getProperty("google.authorization.code"));
String commaSeparatedScopes = env.getProperty("google.auth.scope");
details.setScope(parseScopes(commaSeparatedScopes));
details.setPreEstablishedRedirectUri(env.getProperty("google.preestablished.redirect.url"));
details.setUseCurrentUri(false);
details.setAuthenticationScheme(AuthenticationScheme.query);
details.setClientAuthenticationScheme(AuthenticationScheme.form);
return details;
}
private List<String> parseScopes(String commaSeparatedScopes) {
List<String> scopes = newArrayList();
Collections.addAll(scopes, commaSeparatedScopes.split(","));
return scopes;
}
@Bean
@Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES)
public OAuth2RestTemplate googleRestTemplate() {
return new OAuth2RestTemplate(googleResource(), new DefaultOAuth2ClientContext(accessTokenRequest));
}
}
I had to override some of the Spring classes as the format of the token from Google and the one expected by Spring don't match. So there is some custom handiwork required there.
Upvotes: 4