diff --git a/core/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java b/core/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java index f3dad82..c298052 100644 --- a/core/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java +++ b/core/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java @@ -33,6 +33,7 @@ import java.util.function.Consumer; * * @author Joe Grandja * @author Krisztian Toth + * @author Madhu Bhat * @since 0.0.1 * @see RegisteredClient * @see OAuth2AccessToken @@ -74,6 +75,15 @@ public class OAuth2Authorization implements Serializable { return this.accessToken; } + /** + * Sets the access token {@link OAuth2AccessToken} in the {@link OAuth2Authorization}. + * + * @param accessToken the access token + */ + public final void setAccessToken(OAuth2AccessToken accessToken) { + this.accessToken = accessToken; + } + /** * Returns the attribute(s) associated to the authorization. * diff --git a/core/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationToken.java b/core/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationToken.java index 0c19b6b..dbf28ce 100644 --- a/core/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationToken.java @@ -25,6 +25,7 @@ import java.util.Collections; /** * @author Joe Grandja + * @author Madhu Bhat */ public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; @@ -49,4 +50,13 @@ public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthentication public Object getPrincipal() { return null; } + + /** + * Returns the access token {@link OAuth2AccessToken}. + * + * @return the access token + */ + public OAuth2AccessToken getAccessToken() { + return this.accessToken; + } } diff --git a/core/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationToken.java b/core/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationToken.java index d6a1779..aaa567b 100644 --- a/core/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationToken.java @@ -24,6 +24,7 @@ import java.util.Collections; /** * @author Joe Grandja + * @author Madhu Bhat */ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; @@ -57,4 +58,13 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti public Object getPrincipal() { return null; } + + /** + * Returns the code. + * + * @return the code + */ + public String getCode() { + return this.code; + } } diff --git a/core/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java b/core/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java index 7b2765a..8bf8f28 100644 --- a/core/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java +++ b/core/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java @@ -15,10 +15,31 @@ */ package org.springframework.security.oauth2.server.authorization.web; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.core.convert.converter.Converter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.TokenType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; @@ -26,19 +47,145 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.Writer; /** + * This {@code Filter} is used by the client to obtain an access token by presenting + * its authorization grant. + * + *

+ * It converts the OAuth 2.0 Access Token Request to {@link OAuth2AuthorizationCodeAuthenticationToken}, + * which is then authenticated by the {@link AuthenticationManager} and gets back + * {@link OAuth2AccessTokenAuthenticationToken} which has the {@link OAuth2AccessToken} if the request + * was successfully authenticated. The {@link OAuth2AccessToken} is then updated in the in-flight {@link OAuth2Authorization} + * and sent back to the client. In case the authentication fails, an HTTP 401 (Unauthorized) response is returned. + * + *

+ * By default, this {@code Filter} responds to access token requests + * at the {@code URI} {@code /oauth2/token} and {@code HttpMethod} {@code POST} + * using the default {@link AntPathRequestMatcher}. + * + *

+ * The default base {@code URI} {@code /oauth2/token} may be overridden + * via the constructor {@link #OAuth2TokenEndpointFilter(OAuth2AuthorizationService, AuthenticationManager, String)}. + * * @author Joe Grandja + * @author Madhu Bhat */ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter { - private Converter authorizationGrantConverter; + /** + * The default endpoint {@code URI} for access token requests. + */ + private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token"; + + private Converter authorizationGrantConverter = this::convert; private AuthenticationManager authenticationManager; private OAuth2AuthorizationService authorizationService; + private RequestMatcher uriMatcher; + private ObjectMapper objectMapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL); + + /** + * Constructs an {@code OAuth2TokenEndpointFilter} using the provided parameters. + * + * @param authorizationService the authorization service implementation + * @param authenticationManager the authentication manager implementation + */ + public OAuth2TokenEndpointFilter(OAuth2AuthorizationService authorizationService, AuthenticationManager authenticationManager) { + Assert.notNull(authorizationService, "authorizationService cannot be null"); + Assert.notNull(authenticationManager, "authenticationManager cannot be null"); + this.authenticationManager = authenticationManager; + this.authorizationService = authorizationService; + this.uriMatcher = new AntPathRequestMatcher(DEFAULT_TOKEN_ENDPOINT_URI, HttpMethod.POST.name()); + } + + /** + * Constructs an {@code OAuth2TokenEndpointFilter} using the provided parameters. + * + * @param authorizationService the authorization service implementation + * @param authenticationManager the authentication manager implementation + * @param tokenEndpointUri the token endpoint's uri + */ + public OAuth2TokenEndpointFilter(OAuth2AuthorizationService authorizationService, AuthenticationManager authenticationManager, + String tokenEndpointUri) { + Assert.notNull(authorizationService, "authorizationService cannot be null"); + Assert.notNull(authenticationManager, "authenticationManager cannot be null"); + Assert.hasText(tokenEndpointUri, "tokenEndpointUri cannot be empty"); + this.authenticationManager = authenticationManager; + this.authorizationService = authorizationService; + this.uriMatcher = new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name()); + } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + if (uriMatcher.matches(request)) { + try { + if (validateAccessTokenRequest(request)) { + OAuth2AuthorizationCodeAuthenticationToken authCodeAuthToken = + (OAuth2AuthorizationCodeAuthenticationToken) authorizationGrantConverter.convert(request); + OAuth2AccessTokenAuthenticationToken accessTokenAuthenticationToken = + (OAuth2AccessTokenAuthenticationToken) authenticationManager.authenticate(authCodeAuthToken); + if (accessTokenAuthenticationToken.isAuthenticated()) { + OAuth2Authorization authorization = authorizationService + .findByTokenAndTokenType(authCodeAuthToken.getCode(), TokenType.AUTHORIZATION_CODE); + authorization.setAccessToken(accessTokenAuthenticationToken.getAccessToken()); + authorizationService.save(authorization); + writeSuccessResponse(response, accessTokenAuthenticationToken.getAccessToken()); + } else { + throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT)); + } + } + } catch (OAuth2AuthenticationException exception) { + SecurityContextHolder.clearContext(); + writeFailureResponse(response, exception.getError()); + } + } else { + filterChain.doFilter(request, response); + } + } + private boolean validateAccessTokenRequest(HttpServletRequest request) { + if (StringUtils.isEmpty(request.getParameter(OAuth2ParameterNames.CODE)) + || StringUtils.isEmpty(request.getParameter(OAuth2ParameterNames.REDIRECT_URI)) + || StringUtils.isEmpty(request.getParameter(OAuth2ParameterNames.GRANT_TYPE))) { + throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST)); + } else if (!AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(request.getParameter(OAuth2ParameterNames.GRANT_TYPE))) { + throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE)); + } + return true; + } + + private OAuth2AuthorizationCodeAuthenticationToken convert(HttpServletRequest request) { + Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); + return new OAuth2AuthorizationCodeAuthenticationToken( + request.getParameter(OAuth2ParameterNames.CODE), + clientPrincipal, + request.getParameter(OAuth2ParameterNames.REDIRECT_URI) + ); + } + + private void writeSuccessResponse(HttpServletResponse response, OAuth2AccessToken body) throws IOException { + try (Writer out = response.getWriter()) { + response.setStatus(HttpStatus.OK.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store"); + response.setHeader(HttpHeaders.PRAGMA, "no-cache"); + out.write(objectMapper.writeValueAsString(body)); + } + } + + private void writeFailureResponse(HttpServletResponse response, OAuth2Error error) throws IOException { + try (Writer out = response.getWriter()) { + if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_CLIENT)) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + } else { + response.setStatus(HttpStatus.BAD_REQUEST.value()); + } + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + out.write(objectMapper.writeValueAsString(error)); + } } } diff --git a/core/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java b/core/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java new file mode 100644 index 0000000..0deddbe --- /dev/null +++ b/core/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java @@ -0,0 +1,230 @@ +/* + * Copyright 2020 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.web; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.TokenType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link OAuth2TokenEndpointFilter}. + * + * @author Madhu Bhat + */ +public class OAuth2TokenEndpointFilterTests { + + private OAuth2TokenEndpointFilter filter; + private OAuth2AuthorizationService authorizationService = mock(OAuth2AuthorizationService.class); + private AuthenticationManager authenticationManager = mock(AuthenticationManager.class); + private FilterChain filterChain = mock(FilterChain.class); + private String requestUri; + private static final RegisteredClient REGISTERED_CLIENT = TestRegisteredClients.registeredClient().build(); + private static final String PRINCIPAL_NAME = "principal"; + private static final String AUTHORIZATION_CODE = "code"; + + @Before + public void setUp() { + this.filter = new OAuth2TokenEndpointFilter(this.authorizationService, this.authenticationManager); + this.requestUri = "/oauth2/token"; + } + + @Test + public void constructorServiceAndManagerWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> { + new OAuth2TokenEndpointFilter(null, null); + }).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorServiceAndManagerAndEndpointWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> { + new OAuth2TokenEndpointFilter(null, null, null); + }).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void doFilterWhenNotTokenRequestThenNextFilter() throws Exception { + this.requestUri = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", this.requestUri); + request.setServletPath(this.requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + + this.filter.doFilter(request, response, this.filterChain); + + verify(this.filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void doFilterWhenAccessTokenRequestWithoutGrantTypeThenRespondWithBadRequest() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("POST", this.requestUri); + request.addParameter(OAuth2ParameterNames.CODE, "testAuthCode"); + request.addParameter(OAuth2ParameterNames.REDIRECT_URI, "testRedirectUri"); + request.setServletPath(this.requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + + this.filter.doFilter(request, response, this.filterChain); + + verifyNoInteractions(this.filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + assertThat(response.getContentAsString()).isEqualTo("{\"errorCode\":\"invalid_request\"}"); + } + + @Test + public void doFilterWhenAccessTokenRequestWithoutCodeThenRespondWithBadRequest() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("POST", this.requestUri); + request.addParameter(OAuth2ParameterNames.GRANT_TYPE, "testGrantType"); + request.addParameter(OAuth2ParameterNames.REDIRECT_URI, "testRedirectUri"); + request.setServletPath(this.requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + + this.filter.doFilter(request, response, this.filterChain); + + verifyNoInteractions(this.filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + assertThat(response.getContentAsString()).isEqualTo("{\"errorCode\":\"invalid_request\"}"); + } + + @Test + public void doFilterWhenAccessTokenRequestWithoutRedirectUriThenRespondWithBadRequest() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("POST", this.requestUri); + request.addParameter(OAuth2ParameterNames.GRANT_TYPE, "testGrantType"); + request.addParameter(OAuth2ParameterNames.CODE, "testAuthCode"); + request.setServletPath(this.requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + + this.filter.doFilter(request, response, this.filterChain); + + verifyNoInteractions(this.filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + assertThat(response.getContentAsString()).isEqualTo("{\"errorCode\":\"invalid_request\"}"); + } + + @Test + public void doFilterWhenAccessTokenRequestWithoutAuthCodeGrantTypeThenRespondWithBadRequest() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("POST", this.requestUri); + request.addParameter(OAuth2ParameterNames.GRANT_TYPE, "testGrantType"); + request.addParameter(OAuth2ParameterNames.CODE, "testAuthCode"); + request.addParameter(OAuth2ParameterNames.REDIRECT_URI, "testRedirectUri"); + request.setServletPath(this.requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + + this.filter.doFilter(request, response, this.filterChain); + + verifyNoInteractions(this.filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + assertThat(response.getContentAsString()).isEqualTo("{\"errorCode\":\"unsupported_grant_type\"}"); + } + + @Test + public void doFilterWhenAccessTokenRequestIsNotAuthenticatedThenRespondWithUnauthorized() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("POST", this.requestUri); + request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); + request.addParameter(OAuth2ParameterNames.CODE, "testAuthCode"); + request.addParameter(OAuth2ParameterNames.REDIRECT_URI, "testRedirectUri"); + request.setServletPath(this.requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + Authentication clientPrincipal = mock(Authentication.class); + RegisteredClient registeredClient = mock(RegisteredClient.class); + + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, "testToken", Instant.now().minusSeconds(60), Instant.now()); + OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT) + .principalName(PRINCIPAL_NAME) + .attribute(OAuth2AuthorizationAttributeNames.CODE, AUTHORIZATION_CODE) + .build(); + OAuth2AccessTokenAuthenticationToken accessTokenAuthenticationToken = + new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken); + accessTokenAuthenticationToken.setAuthenticated(false); + + when(this.authorizationService.findByTokenAndTokenType(anyString(), any(TokenType.class))).thenReturn(authorization); + when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(accessTokenAuthenticationToken); + + this.filter.doFilter(request, response, this.filterChain); + + verifyNoInteractions(this.filterChain); + verify(this.authorizationService, times(0)).save(authorization); + verify(this.authenticationManager, times(1)).authenticate(any(Authentication.class)); + assertThat(response.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); + assertThat(response.getContentAsString()) + .isEqualTo("{\"errorCode\":\"invalid_client\"}"); + } + + @Test + public void doFilterWhenValidAccessTokenRequestThenRespondWithAccessToken() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("POST", this.requestUri); + request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); + request.addParameter(OAuth2ParameterNames.CODE, "testAuthCode"); + request.addParameter(OAuth2ParameterNames.REDIRECT_URI, "testRedirectUri"); + request.setServletPath(this.requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + Authentication clientPrincipal = mock(Authentication.class); + RegisteredClient registeredClient = mock(RegisteredClient.class); + + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, "testToken", Instant.now().minusSeconds(60), Instant.now()); + OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT) + .principalName(PRINCIPAL_NAME) + .attribute(OAuth2AuthorizationAttributeNames.CODE, AUTHORIZATION_CODE) + .build(); + OAuth2AccessTokenAuthenticationToken accessTokenAuthenticationToken = + new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken); + accessTokenAuthenticationToken.setAuthenticated(true); + + when(this.authorizationService.findByTokenAndTokenType(anyString(), any(TokenType.class))).thenReturn(authorization); + when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(accessTokenAuthenticationToken); + + this.filter.doFilter(request, response, this.filterChain); + + verifyNoInteractions(this.filterChain); + verify(this.authorizationService, times(1)).save(authorization); + verify(this.authenticationManager, times(1)).authenticate(any(Authentication.class)); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(response.getContentAsString()).contains("\"tokenValue\":\"testToken\""); + assertThat(response.getContentAsString()).contains("\"tokenType\":{\"value\":\"Bearer\"}"); + assertThat(response.getHeader(HttpHeaders.CACHE_CONTROL)).isEqualTo("no-store"); + assertThat(response.getHeader(HttpHeaders.PRAGMA)).isEqualTo("no-cache"); + } +}