openid scope does not require user consent
Closes gh-225
This commit is contained in:
parent
ece5f2b3b1
commit
b5d47366ad
@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.authentication;
|
|||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@ -145,7 +146,8 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
|
|||||||
|
|
||||||
JoseHeader.Builder headersBuilder = JwtUtils.headers();
|
JoseHeader.Builder headersBuilder = JwtUtils.headers();
|
||||||
JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims(
|
JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims(
|
||||||
registeredClient, issuer, authorization.getPrincipalName(), authorizedScopes);
|
registeredClient, issuer, authorization.getPrincipalName(),
|
||||||
|
excludeOpenidIfNecessary(authorizedScopes));
|
||||||
|
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder)
|
JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder)
|
||||||
@ -167,7 +169,7 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
|
|||||||
|
|
||||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
jwtAccessToken.getTokenValue(), jwtAccessToken.getIssuedAt(),
|
jwtAccessToken.getTokenValue(), jwtAccessToken.getIssuedAt(),
|
||||||
jwtAccessToken.getExpiresAt(), authorizedScopes);
|
jwtAccessToken.getExpiresAt(), excludeOpenidIfNecessary(authorizedScopes));
|
||||||
|
|
||||||
OAuth2RefreshToken refreshToken = null;
|
OAuth2RefreshToken refreshToken = null;
|
||||||
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
|
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
|
||||||
@ -243,6 +245,15 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
|
|||||||
registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
|
registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Set<String> excludeOpenidIfNecessary(Set<String> scopes) {
|
||||||
|
if (!scopes.contains(OidcScopes.OPENID)) {
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
|
scopes = new HashSet<>(scopes);
|
||||||
|
scopes.remove(OidcScopes.OPENID);
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supports(Class<?> authentication) {
|
public boolean supports(Class<?> authentication) {
|
||||||
return OAuth2AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
|
return OAuth2AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
|
||||||
|
@ -198,7 +198,7 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
|||||||
.attribute(Principal.class.getName(), principal)
|
.attribute(Principal.class.getName(), principal)
|
||||||
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest);
|
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest);
|
||||||
|
|
||||||
if (registeredClient.getClientSettings().requireUserConsent()) {
|
if (requireUserConsent(registeredClient, authorizationRequest)) {
|
||||||
String state = this.stateGenerator.generateKey();
|
String state = this.stateGenerator.generateKey();
|
||||||
OAuth2Authorization authorization = builder
|
OAuth2Authorization authorization = builder
|
||||||
.attribute(OAuth2ParameterNames.STATE, state)
|
.attribute(OAuth2ParameterNames.STATE, state)
|
||||||
@ -232,6 +232,15 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean requireUserConsent(RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
|
||||||
|
// openid scope does not require consent
|
||||||
|
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID) &&
|
||||||
|
authorizationRequest.getScopes().size() == 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return registeredClient.getClientSettings().requireUserConsent();
|
||||||
|
}
|
||||||
|
|
||||||
private void processUserConsent(HttpServletRequest request, HttpServletResponse response)
|
private void processUserConsent(HttpServletRequest request, HttpServletResponse response)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
@ -264,11 +273,16 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
|||||||
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
|
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
|
||||||
OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
|
OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
|
||||||
this.codeGenerator.generateKey(), issuedAt, expiresAt);
|
this.codeGenerator.generateKey(), issuedAt, expiresAt);
|
||||||
|
Set<String> authorizedScopes = userConsentRequestContext.getScopes();
|
||||||
|
if (userConsentRequestContext.getAuthorizationRequest().getScopes().contains(OidcScopes.OPENID)) {
|
||||||
|
// openid scope is auto-approved as it does not require consent
|
||||||
|
authorizedScopes.add(OidcScopes.OPENID);
|
||||||
|
}
|
||||||
OAuth2Authorization authorization = OAuth2Authorization.from(userConsentRequestContext.getAuthorization())
|
OAuth2Authorization authorization = OAuth2Authorization.from(userConsentRequestContext.getAuthorization())
|
||||||
.token(authorizationCode)
|
.token(authorizationCode)
|
||||||
.attributes(attrs -> {
|
.attributes(attrs -> {
|
||||||
attrs.remove(OAuth2ParameterNames.STATE);
|
attrs.remove(OAuth2ParameterNames.STATE);
|
||||||
attrs.put(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, userConsentRequestContext.getScopes());
|
attrs.put(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
this.authorizationService.save(authorization);
|
this.authorizationService.save(authorization);
|
||||||
@ -661,6 +675,8 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
||||||
OAuth2AuthorizationRequest.class.getName());
|
OAuth2AuthorizationRequest.class.getName());
|
||||||
|
Set<String> scopes = new HashSet<>(authorizationRequest.getScopes());
|
||||||
|
scopes.remove(OidcScopes.OPENID); // openid scope does not require consent
|
||||||
String state = authorization.getAttribute(
|
String state = authorization.getAttribute(
|
||||||
OAuth2ParameterNames.STATE);
|
OAuth2ParameterNames.STATE);
|
||||||
|
|
||||||
@ -695,7 +711,7 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
|||||||
builder.append(" <input type=\"hidden\" name=\"client_id\" value=\"" + registeredClient.getClientId() + "\">");
|
builder.append(" <input type=\"hidden\" name=\"client_id\" value=\"" + registeredClient.getClientId() + "\">");
|
||||||
builder.append(" <input type=\"hidden\" name=\"state\" value=\"" + state + "\">");
|
builder.append(" <input type=\"hidden\" name=\"state\" value=\"" + state + "\">");
|
||||||
|
|
||||||
for (String scope : authorizationRequest.getScopes()) {
|
for (String scope : scopes) {
|
||||||
builder.append(" <div class=\"form-group form-check py-1\">");
|
builder.append(" <div class=\"form-group form-check py-1\">");
|
||||||
builder.append(" <input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" value=\"" + scope + "\" id=\"" + scope + "\" checked>");
|
builder.append(" <input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" value=\"" + scope + "\" id=\"" + scope + "\" checked>");
|
||||||
builder.append(" <label class=\"form-check-label\" for=\"" + scope + "\">" + scope + "</label>");
|
builder.append(" <label class=\"form-check-label\" for=\"" + scope + "\">" + scope + "</label>");
|
||||||
|
@ -19,6 +19,9 @@ import java.security.Principal;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@ -306,6 +309,9 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
|||||||
assertThat(accessTokenContext.<OAuth2AuthorizationGrantAuthenticationToken>getAuthorizationGrant()).isEqualTo(authentication);
|
assertThat(accessTokenContext.<OAuth2AuthorizationGrantAuthenticationToken>getAuthorizationGrant()).isEqualTo(authentication);
|
||||||
assertThat(accessTokenContext.getHeaders()).isNotNull();
|
assertThat(accessTokenContext.getHeaders()).isNotNull();
|
||||||
assertThat(accessTokenContext.getClaims()).isNotNull();
|
assertThat(accessTokenContext.getClaims()).isNotNull();
|
||||||
|
Map<String, Object> claims = new HashMap<>();
|
||||||
|
accessTokenContext.getClaims().claims(claims::putAll);
|
||||||
|
assertThat(claims.containsKey(OidcScopes.OPENID)).isFalse();
|
||||||
// ID Token context
|
// ID Token context
|
||||||
JwtEncodingContext idTokenContext = jwtEncodingContextCaptor.getAllValues().get(1);
|
JwtEncodingContext idTokenContext = jwtEncodingContextCaptor.getAllValues().get(1);
|
||||||
assertThat(idTokenContext.getRegisteredClient()).isEqualTo(registeredClient);
|
assertThat(idTokenContext.getRegisteredClient()).isEqualTo(registeredClient);
|
||||||
@ -328,8 +334,9 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
|||||||
assertThat(accessTokenAuthentication.getRegisteredClient().getId()).isEqualTo(updatedAuthorization.getRegisteredClientId());
|
assertThat(accessTokenAuthentication.getRegisteredClient().getId()).isEqualTo(updatedAuthorization.getRegisteredClientId());
|
||||||
assertThat(accessTokenAuthentication.getPrincipal()).isEqualTo(clientPrincipal);
|
assertThat(accessTokenAuthentication.getPrincipal()).isEqualTo(clientPrincipal);
|
||||||
assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken().getToken());
|
assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken().getToken());
|
||||||
assertThat(accessTokenAuthentication.getAccessToken().getScopes())
|
Set<String> accessTokenScopes = new HashSet<>(updatedAuthorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME));
|
||||||
.isEqualTo(authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME));
|
accessTokenScopes.remove(OidcScopes.OPENID);
|
||||||
|
assertThat(accessTokenAuthentication.getAccessToken().getScopes()).isEqualTo(accessTokenScopes);
|
||||||
assertThat(accessTokenAuthentication.getRefreshToken()).isNotNull();
|
assertThat(accessTokenAuthentication.getRefreshToken()).isNotNull();
|
||||||
assertThat(accessTokenAuthentication.getRefreshToken()).isEqualTo(updatedAuthorization.getRefreshToken().getToken());
|
assertThat(accessTokenAuthentication.getRefreshToken()).isEqualTo(updatedAuthorization.getRefreshToken().getToken());
|
||||||
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = updatedAuthorization.getToken(OAuth2AuthorizationCode.class);
|
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = updatedAuthorization.getToken(OAuth2AuthorizationCode.class);
|
||||||
|
@ -51,6 +51,7 @@ import org.springframework.security.oauth2.server.authorization.TestOAuth2Author
|
|||||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
|
||||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2AuthorizationCode;
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2AuthorizationCode;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
@ -445,6 +446,19 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
|||||||
doFilterWhenAuthorizationRequestThenAuthorizationResponse(registeredClient, request);
|
doFilterWhenAuthorizationRequestThenAuthorizationResponse(registeredClient, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doFilterWhenAuthenticationRequestIncludesOnlyOpenidScopeThenDoesNotRequireConsent() throws Exception {
|
||||||
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||||
|
.scopes(scopes -> {
|
||||||
|
scopes.clear();
|
||||||
|
scopes.add(OidcScopes.OPENID);
|
||||||
|
})
|
||||||
|
.clientSettings(ClientSettings::requireUserConsent)
|
||||||
|
.build();
|
||||||
|
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
|
||||||
|
doFilterWhenAuthorizationRequestThenAuthorizationResponse(registeredClient, request);
|
||||||
|
}
|
||||||
|
|
||||||
private void doFilterWhenAuthorizationRequestThenAuthorizationResponse(
|
private void doFilterWhenAuthorizationRequestThenAuthorizationResponse(
|
||||||
RegisteredClient registeredClient, MockHttpServletRequest request) throws Exception {
|
RegisteredClient registeredClient, MockHttpServletRequest request) throws Exception {
|
||||||
|
|
||||||
@ -772,11 +786,12 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void doFilterWhenUserConsentRequestApprovedThenAuthorizationResponse() throws Exception {
|
public void doFilterWhenUserConsentRequestApprovedThenAuthorizationResponse() throws Exception {
|
||||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
|
||||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||||
.thenReturn(registeredClient);
|
.thenReturn(registeredClient);
|
||||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
|
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
|
||||||
.principalName(this.authentication.getName())
|
.principalName(this.authentication.getName())
|
||||||
|
.attributes(attrs -> attrs.remove(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME))
|
||||||
.build();
|
.build();
|
||||||
when(this.authorizationService.findByToken(eq("state"), eq(STATE_TOKEN_TYPE)))
|
when(this.authorizationService.findByToken(eq("state"), eq(STATE_TOKEN_TYPE)))
|
||||||
.thenReturn(authorization);
|
.thenReturn(authorization);
|
||||||
@ -908,8 +923,10 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
|||||||
request.addParameter(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
|
request.addParameter(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
|
||||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||||
for (String scope : registeredClient.getScopes()) {
|
for (String scope : registeredClient.getScopes()) {
|
||||||
|
if (!OidcScopes.OPENID.equals(scope)) {
|
||||||
request.addParameter(OAuth2ParameterNames.SCOPE, scope);
|
request.addParameter(OAuth2ParameterNames.SCOPE, scope);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
request.addParameter("consent_action", "approve");
|
request.addParameter("consent_action", "approve");
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
|
Loading…
Reference in New Issue
Block a user