From b62b161b95a603a0bc5baed7a9ac21691b119192 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Wed, 23 Jun 2021 08:30:21 +0200 Subject: [PATCH] Include WebAuthenticationDetails in token requests Closes gh-322 --- .../web/OAuth2ClientAuthenticationFilter.java | 26 ++++++++++++------ .../web/OAuth2TokenEndpointFilter.java | 9 +++++++ ...OAuth2ClientAuthenticationFilterTests.java | 27 +++++++++++++++---- .../web/OAuth2TokenEndpointFilterTests.java | 19 +++++++++++++ 4 files changed, 68 insertions(+), 13 deletions(-) diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java index a589df1d..8ea9a511 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,19 @@ */ package org.springframework.security.oauth2.server.authorization.web; +import java.io.IOException; +import java.util.Arrays; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -32,17 +42,11 @@ import org.springframework.security.oauth2.server.authorization.authentication.O import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Arrays; - /** * A {@code Filter} that processes an authentication request for an OAuth 2.0 Client. * @@ -58,6 +62,8 @@ public class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter { private final AuthenticationManager authenticationManager; private final RequestMatcher requestMatcher; private final HttpMessageConverter errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter(); + private final AuthenticationDetailsSource authenticationDetailsSource = + new WebAuthenticationDetailsSource(); private AuthenticationConverter authenticationConverter; private AuthenticationSuccessHandler authenticationSuccessHandler; private AuthenticationFailureHandler authenticationFailureHandler; @@ -90,6 +96,10 @@ public class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter { if (this.requestMatcher.matches(request)) { try { Authentication authenticationRequest = this.authenticationConverter.convert(request); + if (authenticationRequest instanceof AbstractAuthenticationToken) { + ((AbstractAuthenticationToken) authenticationRequest).setDetails( + this.authenticationDetailsSource.buildDetails(request)); + } if (authenticationRequest != null) { Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest); this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java index b0cb4842..5500e18f 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java @@ -29,6 +29,8 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -53,6 +55,7 @@ import org.springframework.security.oauth2.server.authorization.web.authenticati import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -101,6 +104,8 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter { new OAuth2AccessTokenResponseHttpMessageConverter(); private final HttpMessageConverter errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter(); + private final AuthenticationDetailsSource authenticationDetailsSource = + new WebAuthenticationDetailsSource(); private AuthenticationConverter authenticationConverter; private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendAccessTokenResponse; private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse; @@ -151,6 +156,10 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter { if (authorizationGrantAuthentication == null) { throwError(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE, OAuth2ParameterNames.GRANT_TYPE); } + if (authorizationGrantAuthentication instanceof AbstractAuthenticationToken) { + ((AbstractAuthenticationToken) authorizationGrantAuthentication) + .setDetails(this.authenticationDetailsSource.buildDetails(request)); + } OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java index bf778cce..2404406f 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,15 @@ */ package org.springframework.security.oauth2.server.authorization.web; +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; + import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageConverter; @@ -36,15 +42,13 @@ import org.springframework.security.oauth2.server.authorization.authentication.O import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; -import javax.servlet.FilterChain; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -184,6 +188,8 @@ public class OAuth2ClientAuthenticationFilterTests { @Test public void doFilterWhenRequestMatchesAndValidCredentialsThenProcessed() throws Exception { + final String remoteAddress = "remote-address"; + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); when(this.authenticationConverter.convert(any(HttpServletRequest.class))).thenReturn( new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), registeredClient.getClientSecret(), ClientAuthenticationMethod.BASIC, null)); @@ -192,6 +198,7 @@ public class OAuth2ClientAuthenticationFilterTests { MockHttpServletRequest request = new MockHttpServletRequest("POST", this.filterProcessesUrl); request.setServletPath(this.filterProcessesUrl); + request.setRemoteAddr(remoteAddress); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = mock(FilterChain.class); @@ -202,6 +209,16 @@ public class OAuth2ClientAuthenticationFilterTests { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); assertThat(authentication).isInstanceOf(OAuth2ClientAuthenticationToken.class); assertThat(((OAuth2ClientAuthenticationToken) authentication).getRegisteredClient()).isEqualTo(registeredClient); + + ArgumentCaptor authenticationRequestCaptor = + ArgumentCaptor.forClass(OAuth2ClientAuthenticationToken.class); + verify(this.authenticationManager).authenticate(authenticationRequestCaptor.capture()); + assertThat(authenticationRequestCaptor) + .extracting(ArgumentCaptor::getValue) + .extracting(OAuth2ClientAuthenticationToken::getDetails) + .asInstanceOf(type(WebAuthenticationDetails.class)) + .extracting(WebAuthenticationDetails::getRemoteAddress) + .isEqualTo(remoteAddress); } private OAuth2Error readError(MockHttpServletResponse response) throws Exception { diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java index f5cbf151..6654488a 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java @@ -60,11 +60,13 @@ import org.springframework.security.oauth2.server.authorization.client.TestRegis import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; +import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -79,6 +81,7 @@ import static org.mockito.Mockito.when; * @author Daniel Garnier-Moiroux */ public class OAuth2TokenEndpointFilterTests { + private static final String REMOTE_ADDRESS = "remote-address"; private AuthenticationManager authenticationManager; private OAuth2TokenEndpointFilter filter; private final HttpMessageConverter errorHttpResponseConverter = @@ -260,6 +263,10 @@ public class OAuth2TokenEndpointFilterTests { request.getParameter(OAuth2ParameterNames.REDIRECT_URI)); assertThat(authorizationCodeAuthentication.getAdditionalParameters()) .containsExactly(entry("custom-param-1", "custom-value-1")); + assertThat(authorizationCodeAuthentication.getDetails()) + .asInstanceOf(type(WebAuthenticationDetails.class)) + .extracting(WebAuthenticationDetails::getRemoteAddress) + .isEqualTo(REMOTE_ADDRESS); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); OAuth2AccessTokenResponse accessTokenResponse = readAccessTokenResponse(response); @@ -322,6 +329,10 @@ public class OAuth2TokenEndpointFilterTests { assertThat(clientCredentialsAuthentication.getScopes()).isEqualTo(registeredClient.getScopes()); assertThat(clientCredentialsAuthentication.getAdditionalParameters()) .containsExactly(entry("custom-param-1", "custom-value-1")); + assertThat(clientCredentialsAuthentication.getDetails()) + .asInstanceOf(type(WebAuthenticationDetails.class)) + .extracting(WebAuthenticationDetails::getRemoteAddress) + .isEqualTo(REMOTE_ADDRESS); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); OAuth2AccessTokenResponse accessTokenResponse = readAccessTokenResponse(response); @@ -404,6 +415,11 @@ public class OAuth2TokenEndpointFilterTests { assertThat(refreshTokenAuthenticationToken.getScopes()).isEqualTo(registeredClient.getScopes()); assertThat(refreshTokenAuthenticationToken.getAdditionalParameters()) .containsExactly(entry("custom-param-1", "custom-value-1")); + assertThat(refreshTokenAuthenticationToken.getDetails()) + .asInstanceOf(type(WebAuthenticationDetails.class)) + .extracting(WebAuthenticationDetails::getRemoteAddress) + .isEqualTo(REMOTE_ADDRESS); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); OAuth2AccessTokenResponse accessTokenResponse = readAccessTokenResponse(response); @@ -536,6 +552,7 @@ public class OAuth2TokenEndpointFilterTests { String requestUri = OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri); request.setServletPath(requestUri); + request.setRemoteAddr(REMOTE_ADDRESS); request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); request.addParameter(OAuth2ParameterNames.CODE, "code"); @@ -551,6 +568,7 @@ public class OAuth2TokenEndpointFilterTests { String requestUri = OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri); request.setServletPath(requestUri); + request.setRemoteAddr(REMOTE_ADDRESS); request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()); request.addParameter(OAuth2ParameterNames.SCOPE, @@ -564,6 +582,7 @@ public class OAuth2TokenEndpointFilterTests { String requestUri = OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri); request.setServletPath(requestUri); + request.setRemoteAddr(REMOTE_ADDRESS); request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.REFRESH_TOKEN.getValue()); request.addParameter(OAuth2ParameterNames.REFRESH_TOKEN, "refresh-token");