Skip to content

Remember user consent and make consent page configurable #280

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
Expand Down Expand Up @@ -107,6 +109,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
this.oidcProviderConfigurationEndpointMatcher.matches(request) ||
this.authorizationServerMetadataEndpointMatcher.matches(request) ||
this.oidcClientRegistrationEndpointMatcher.matches(request);
private String consentPage;

/**
* Sets the repository of registered clients.
Expand All @@ -132,6 +135,18 @@ public OAuth2AuthorizationServerConfigurer<B> authorizationService(OAuth2Authori
return this;
}

/**
* Sets the authorization consent service.
*
* @param authorizationConsentService the authorization service
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer<B> authorizationConsentService(OAuth2AuthorizationConsentService authorizationConsentService) {
Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
this.getBuilder().setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
return this;
}

/**
* Sets the provider settings.
*
Expand All @@ -144,6 +159,43 @@ public OAuth2AuthorizationServerConfigurer<B> providerSettings(ProviderSettings
return this;
}

/**
* Specify the URL to redirect Resource Owners to if consent is required during
* the {@code authorization_code} flow. A default consent page will be generated when
* this attribute is not specified.
*
* If a URL is specified, users are required to process the specified URL to generate
* a consent page. The query string will contain the following parameters:
*
* <ul>
* <li>{@code client_id} the client identifier</li>
* <li>{@code scope} the space separated list of scopes present in the authorization request</li>
* <li>{@code state} a CSRF protection token</li>
* </ul>
*
* In general, the consent page should create a form that submits
* a request with the following requirements:
*
* <ul>
* <li>It must be an HTTP POST</li>
* <li>It must be submitted to {@link ProviderSettings#authorizationEndpoint()}</li>
* <li>It must include the received {@code client_id} as an HTTP parameter</li>
* <li>It must include the received {@code state} as an HTTP parameter</li>
* <li>It must include the list of {@code scope}s the {@code Resource Owners}
* consents to as an HTTP parameter</li>
* <li>It must include the {@code consent_action} parameter, with value either
* {@code approve} or {@code cancel} as an HTTP parameter</li>
* </ul>
*
*
* @param consentPage the consent page to redirect to if consent is required (e.g. "/consent")
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer<B> consentPage(String consentPage) {
this.consentPage = consentPage;
return this;
}

/**
* Returns a {@link RequestMatcher} for the authorization server endpoints.
*
Expand Down Expand Up @@ -263,7 +315,12 @@ public void configure(B builder) {
new OAuth2AuthorizationEndpointFilter(
getRegisteredClientRepository(builder),
getAuthorizationService(builder),
providerSettings.authorizationEndpoint());
getAuthorizationConsentService(builder),
providerSettings.authorizationEndpoint()
);
if (this.consentPage != null) {
authorizationEndpointFilter.setUserConsentUri(this.consentPage);
}
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);

OAuth2TokenEndpointFilter tokenEndpointFilter =
Expand Down Expand Up @@ -347,6 +404,18 @@ private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService get
return authorizationService;
}

private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationConsentService getAuthorizationConsentService(B builder) {
OAuth2AuthorizationConsentService authorizationConsentService = builder.getSharedObject(OAuth2AuthorizationConsentService.class);
if (authorizationConsentService == null) {
authorizationConsentService = getOptionalBean(builder, OAuth2AuthorizationConsentService.class);
if (authorizationConsentService == null) {
authorizationConsentService = new InMemoryOAuth2AuthorizationConsentService();
}
builder.setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
}
return authorizationConsentService;
}

private static <B extends HttpSecurityBuilder<B>> JwtEncoder getJwtEncoder(B builder) {
JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class);
if (jwtEncoder == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* 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.
* 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;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
* An {@link OAuth2AuthorizationConsentService} that stores {@link OAuth2AuthorizationConsent}'s in-memory.
*
* <p>
* <b>NOTE:</b> This implementation should ONLY be used during development/testing.
*
* @author Daniel Garnier-Moiroux
* @since 0.1.2
* @see OAuth2AuthorizationConsentService
*/
public final class InMemoryOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
private final Map<Integer, OAuth2AuthorizationConsent> authorizationConsents = new ConcurrentHashMap<>();

/**
* Constructs an {@code InMemoryOAuth2AuthorizationConsentService}.
*/
public InMemoryOAuth2AuthorizationConsentService() {
this(Collections.emptyList());
}

/**
* Constructs an {@code InMemoryOAuth2AuthorizationConsentService} using the provided parameters.
*
* @param authorizationConsents the authorization consent(s)
*/
public InMemoryOAuth2AuthorizationConsentService(OAuth2AuthorizationConsent... authorizationConsents) {
this(Arrays.asList(authorizationConsents));
}

/**
* Constructs an {@code InMemoryOAuth2AuthorizationConsentService} using the provided parameters.
*
* @param authorizationConsents the authorization consent(s)
*/
public InMemoryOAuth2AuthorizationConsentService(List<OAuth2AuthorizationConsent> authorizationConsents) {
Assert.notNull(authorizationConsents, "authorizationConsents cannot be null");
authorizationConsents.forEach(authorizationConsent -> {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
int id = getId(authorizationConsent);
Assert.isTrue(!this.authorizationConsents.containsKey(id),
"The authorizationConsent must be unique. Found duplicate, with registered client id: ["
+ authorizationConsent.getRegisteredClientId()
+ "] and principal name: [" + authorizationConsent.getPrincipalName() + "]");
this.authorizationConsents.put(id, authorizationConsent);
});
}

@Override
public void save(OAuth2AuthorizationConsent authorizationConsent) {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
int id = getId(authorizationConsent);
this.authorizationConsents.put(id, authorizationConsent);
}

@Override
public void remove(OAuth2AuthorizationConsent authorizationConsent) {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
int id = getId(authorizationConsent);
this.authorizationConsents.remove(id, authorizationConsent);
}

@Override
@Nullable
public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
int id = getId(registeredClientId, principalName);
return this.authorizationConsents.get(id);
}

private static int getId(String registeredClientId, String principalName) {
return Objects.hash(registeredClientId, principalName);
}

private static int getId(OAuth2AuthorizationConsent authorizationConsent) {
return getId(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
}
}
Loading