Support for user consent (#4)
All clients will now require user consent. Users with developer access can configure their clients to not require consent.
This commit is contained in:
parent
8307bc4906
commit
18945e22bf
src
main
java/se/su/dsv/oauth2
AuthorizationServer.javaConfig.javaEmbeddedConfiguration.javaJDBCClientRepository.javaPersistentConfiguration.java
admin
dev
staging
web
resources
test/java/se/su/dsv/oauth2
@ -9,12 +9,17 @@ import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
|
||||
@ -27,10 +32,12 @@ import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
||||
import se.su.dsv.oauth2.shibboleth.Entitlement;
|
||||
import se.su.dsv.oauth2.shibboleth.ShibbolethConfigurer;
|
||||
import se.su.dsv.oauth2.shibboleth.ShibbolethTokenPopulator;
|
||||
import se.su.dsv.oauth2.staging.SkipConsentForDevelopers;
|
||||
import se.su.dsv.oauth2.staging.StagingSecurityConfigurer;
|
||||
import se.su.dsv.oauth2.web.client.DeveloperAccessCheck;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@SpringBootApplication(proxyBeanMethods = false)
|
||||
@EnableWebSecurity
|
||||
@ -70,7 +77,9 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
||||
/// the session and allow the flow to start.
|
||||
@Bean
|
||||
@Order(1)
|
||||
public SecurityFilterChain oauth2Endpoints(HttpSecurity http, Optional<HttpSecurityCustomizer> customizer)
|
||||
public SecurityFilterChain oauth2Endpoints(
|
||||
HttpSecurity http,
|
||||
List<HttpSecurityCustomizer> customizer)
|
||||
throws Exception
|
||||
{
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
||||
@ -79,6 +88,9 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
||||
http
|
||||
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
|
||||
.with(authorizationServerConfigurer, authorizationServer -> authorizationServer
|
||||
.authorizationEndpoint(authorizeEndpoint -> authorizeEndpoint
|
||||
.consentPage("/oauth2/consent"))
|
||||
.withObjectPostProcessor(enableGivingConsentWithNoScopes())
|
||||
.oidc(Customizer.withDefaults()))
|
||||
.with(new ShibbolethConfigurer(), Customizer.withDefaults())
|
||||
.sessionManagement(session -> session
|
||||
@ -92,7 +104,7 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
||||
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
|
||||
));
|
||||
|
||||
customizer.ifPresent(c -> {
|
||||
customizer.forEach(c -> {
|
||||
try {
|
||||
c.customize(http);
|
||||
} catch (Exception e) {
|
||||
@ -103,6 +115,46 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/// The [OAuth2AuthorizationConsentAuthenticationProvider], which is the component
|
||||
/// that handles incoming consent, requires there to be at least one scope that is
|
||||
/// approved by the user. This is a problem since we want to allow the user to give
|
||||
/// consent without any scopes to enable, for example, just using this service as a
|
||||
/// way to authenticate the end user (getting access to the "sub" claim).
|
||||
///
|
||||
/// To get around this limitation, a dummy scope is added that is always approved.
|
||||
private ObjectPostProcessor<OAuth2AuthorizationConsentAuthenticationProvider> enableGivingConsentWithNoScopes() {
|
||||
return new ObjectPostProcessor<>() {
|
||||
@Override
|
||||
public <O extends OAuth2AuthorizationConsentAuthenticationProvider> O postProcess(final O object) {
|
||||
object.setAuthorizationConsentCustomizer(consentContext ->
|
||||
consentContext.getAuthorizationConsent()
|
||||
.authority(new SimpleGrantedAuthority("CONSENT")));
|
||||
return object;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Disable consent in staging. With the custom authorization in place, the current user is always the developer
|
||||
/// while the authorization request token may contain a custom principal. When Spring Authorization Server attempts
|
||||
/// to validate the submitted consent, it checks that the current user is the same as the authorization request
|
||||
/// token - which it is not. The easiest solution is to disable consent in staging for developers.
|
||||
@Bean
|
||||
@Profile("staging")
|
||||
public HttpSecurityCustomizer disableConsentInStaging(
|
||||
DeveloperAccessCheck developerAccessCheck)
|
||||
{
|
||||
ObjectPostProcessor<OAuth2AuthorizationCodeRequestAuthenticationProvider> postprocessor = new ObjectPostProcessor<>() {
|
||||
@Override
|
||||
public <O extends OAuth2AuthorizationCodeRequestAuthenticationProvider> O postProcess(final O object) {
|
||||
object.setAuthorizationConsentRequired(new SkipConsentForDevelopers(developerAccessCheck));
|
||||
return object;
|
||||
}
|
||||
};
|
||||
return http -> http
|
||||
.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||
.withObjectPostProcessor(postprocessor);
|
||||
}
|
||||
|
||||
public interface HttpSecurityCustomizer {
|
||||
void customize(HttpSecurity http) throws Exception;
|
||||
}
|
||||
@ -123,14 +175,11 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
||||
*/
|
||||
@Bean
|
||||
@Order(2)
|
||||
public SecurityFilterChain defaultSecurityFilterChain(
|
||||
HttpSecurity http,
|
||||
Config config)
|
||||
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
|
||||
throws Exception
|
||||
{
|
||||
|
||||
http.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers("/admin/**").hasAuthority(Entitlement.asAuthority(config.adminEntitlement()))
|
||||
.anyRequest().authenticated());
|
||||
|
||||
http.exceptionHandling(exceptions -> exceptions
|
||||
@ -162,4 +211,21 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
||||
|
||||
return delegatingPasswordEncoder;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DeveloperAccessCheck developerAccessCheck(Config config) {
|
||||
final String developerAuthority = Entitlement.asAuthority(config.developerEntitlement());
|
||||
return authentication -> {
|
||||
if (!authentication.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
for (GrantedAuthority authority : authentication.getAuthorities()) {
|
||||
if (developerAuthority.equals(authority.getAuthority())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package se.su.dsv.oauth2;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties("se.su.dsv.oauth2")
|
||||
public record Config(String adminEntitlement, String developerEntitlement, RSAKeyPair rsaKeyPair) {
|
||||
public record Config(String developerEntitlement, RSAKeyPair rsaKeyPair) {
|
||||
record RSAKeyPair(String kid, String modulus, String privateExponent, String publicExponent) {
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package se.su.dsv.oauth2;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import se.su.dsv.oauth2.admin.repository.ClientRepository;
|
||||
import se.su.dsv.oauth2.admin.repository.ClientRow;
|
||||
|
||||
@ -15,6 +17,11 @@ import java.util.Optional;
|
||||
@Configuration
|
||||
@Profile("embedded")
|
||||
public class EmbeddedConfiguration {
|
||||
@Bean
|
||||
public OAuth2AuthorizationService authorizationService() {
|
||||
return new InMemoryOAuth2AuthorizationService();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ClientRepository clientRepository() {
|
||||
ArrayList<ClientRow> clients = new ArrayList<>();
|
||||
@ -34,7 +41,7 @@ public class EmbeddedConfiguration {
|
||||
String scopeString = System.getenv("CLIENT_SCOPES");
|
||||
|
||||
return new ClientRow(clientId, clientId, clientId, "dev@localhost",
|
||||
redirectUri, scopeString, clientSecret);
|
||||
redirectUri, scopeString, clientSecret, false);
|
||||
}
|
||||
|
||||
private static class InMemoryClientrepository implements ClientRepository {
|
||||
|
@ -29,7 +29,7 @@ public class JDBCClientRepository implements ClientRepository {
|
||||
|
||||
@Override
|
||||
public List<ClientRow> getClients(final Principal owner) {
|
||||
return getJdbc().sql("SELECT id, client_id, name, contact_email, redirect_uri, scopes, client_secret FROM v2_client WHERE id IN (SELECT client_id FROM v2_client_owner WHERE owner = :owner)")
|
||||
return getJdbc().sql("SELECT id, client_id, name, contact_email, redirect_uri, scopes, client_secret, requires_consent FROM v2_client WHERE id IN (SELECT client_id FROM v2_client_owner WHERE owner = :owner)")
|
||||
.param("owner", owner.getName())
|
||||
.query(ClientRow.class)
|
||||
.list();
|
||||
@ -45,7 +45,7 @@ public class JDBCClientRepository implements ClientRepository {
|
||||
|
||||
@Override
|
||||
public Optional<ClientRow> getClientRowById(final String id) {
|
||||
return getJdbc().sql("SELECT id, client_id, name, contact_email, redirect_uri, scopes, client_secret FROM v2_client WHERE id = :id")
|
||||
return getJdbc().sql("SELECT id, client_id, name, contact_email, redirect_uri, scopes, client_secret, requires_consent FROM v2_client WHERE id = :id")
|
||||
.param("id", id)
|
||||
.query(ClientRow.class)
|
||||
.optional();
|
||||
@ -53,7 +53,7 @@ public class JDBCClientRepository implements ClientRepository {
|
||||
|
||||
@Override
|
||||
public Optional<ClientRow> getClientRowByClientId(final String clientId) {
|
||||
return getJdbc().sql("SELECT id, client_id, name, contact_email, redirect_uri, scopes, client_secret FROM v2_client WHERE client_id = :clientId")
|
||||
return getJdbc().sql("SELECT id, client_id, name, contact_email, redirect_uri, scopes, client_secret, requires_consent FROM v2_client WHERE client_id = :clientId")
|
||||
.param("clientId", clientId)
|
||||
.query(ClientRow.class)
|
||||
.optional();
|
||||
@ -62,14 +62,15 @@ public class JDBCClientRepository implements ClientRepository {
|
||||
@Override
|
||||
public void addNewClient(final ClientRow clientRow) {
|
||||
getJdbc().sql("""
|
||||
INSERT INTO v2_client (id, client_id, client_secret, name, redirect_uri, contact_email, scopes)
|
||||
VALUES (:id, :clientId, :clientSecret, :name, :redirectUri, :contactEmail, :scopes)
|
||||
INSERT INTO v2_client (id, client_id, client_secret, name, redirect_uri, contact_email, scopes, requires_consent)
|
||||
VALUES (:id, :clientId, :clientSecret, :name, :redirectUri, :contactEmail, :scopes, :requiresConsent)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
client_id = VALUES(client_id),
|
||||
client_secret = VALUES(client_secret),
|
||||
name = VALUES(name),
|
||||
redirect_uri = VALUES(redirect_uri),
|
||||
contact_email = VALUES(contact_email),
|
||||
requires_consent = VALUES(requires_consent),
|
||||
scopes = VALUES(scopes);
|
||||
""")
|
||||
.paramSource(clientRow)
|
||||
|
@ -3,8 +3,12 @@ package se.su.dsv.oauth2;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.jdbc.core.JdbcOperations;
|
||||
import org.springframework.jdbc.core.simple.JdbcClient;
|
||||
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
|
||||
@Configuration
|
||||
@Profile("!embedded")
|
||||
@ -18,4 +22,12 @@ public class PersistentConfiguration {
|
||||
public OAuth2AuthorizationService authorizationService(JdbcClient jdbcClient) {
|
||||
return new SerializingJDBCOAuth2AuthorizationService(jdbcClient);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OAuth2AuthorizationConsentService authorizationConsentService(
|
||||
JdbcOperations jdbcOperations,
|
||||
RegisteredClientRepository registeredClientRepository)
|
||||
{
|
||||
return new JdbcOAuth2AuthorizationConsentService(jdbcOperations, registeredClientRepository);
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ public record Client(
|
||||
String redirectUri,
|
||||
boolean isPublic,
|
||||
Set<String> scopes,
|
||||
List<String> owners)
|
||||
List<String> owners,
|
||||
boolean requiresConsent)
|
||||
{
|
||||
}
|
||||
|
@ -8,10 +8,11 @@ public record ClientData(
|
||||
URI redirectURI,
|
||||
boolean isPublic,
|
||||
Set<String> scopes,
|
||||
String contactEmail)
|
||||
String contactEmail,
|
||||
boolean requiresConsent)
|
||||
{
|
||||
// When creating a resource server client
|
||||
public ClientData(String clientName, String contactEmail) {
|
||||
this(clientName, null, false, Set.of(), contactEmail);
|
||||
this(clientName, null, false, Set.of(), contactEmail, true);
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme
|
||||
String scopeString = toScopeString(clientData);
|
||||
|
||||
ClientRow clientRow = new ClientRow(id, clientId, clientData.clientName(), clientData.contactEmail(),
|
||||
redirectURI, scopeString, encodedClientSecret);
|
||||
redirectURI, scopeString, encodedClientSecret, clientData.requiresConsent());
|
||||
|
||||
clientRepository.addNewClient(clientRow);
|
||||
|
||||
@ -66,7 +66,7 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme
|
||||
|
||||
ClientRow updated = new ClientRow(id, currentClient.clientId(), clientData.clientName(),
|
||||
clientData.contactEmail(), getRedirectUri(clientData),
|
||||
toScopeString(clientData), currentClient.clientSecret());
|
||||
toScopeString(clientData), currentClient.clientSecret(), clientData.requiresConsent());
|
||||
clientRepository.update(updated);
|
||||
return toClient(updated);
|
||||
}
|
||||
@ -133,7 +133,8 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme
|
||||
clientRow.redirectUri(),
|
||||
isPublic,
|
||||
scopes,
|
||||
owners);
|
||||
owners,
|
||||
clientRow.requiresConsent());
|
||||
}
|
||||
|
||||
// Used by various components of the OAuth 2.0 infrastructure to upgrade
|
||||
@ -193,7 +194,10 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme
|
||||
redirectUris.add(clientRow.redirectUri());
|
||||
}
|
||||
})
|
||||
.clientSettings(ClientSettings.builder().requireProofKey(clientRow.isPublic()).build())
|
||||
.clientSettings(ClientSettings.builder()
|
||||
.requireAuthorizationConsent(clientRow.requiresConsent())
|
||||
.requireProofKey(clientRow.isPublic())
|
||||
.build())
|
||||
.scopes(currentScopes -> currentScopes.addAll(clientRow.scopeSet()))
|
||||
.build();
|
||||
}
|
||||
|
@ -12,7 +12,8 @@ public record ClientRow(
|
||||
String contactEmail,
|
||||
String redirectUri,
|
||||
String scopes,
|
||||
String clientSecret)
|
||||
String clientSecret,
|
||||
boolean requiresConsent)
|
||||
{
|
||||
public Set<String> scopeSet() {
|
||||
if (scopes == null) {
|
||||
|
@ -13,7 +13,7 @@ import se.su.dsv.oauth2.Config;
|
||||
public class DevConfiguration {
|
||||
@Bean
|
||||
public FilterRegistrationBean<HttpFilter> fakeSSO(SecurityProperties securityProperties, Config config) {
|
||||
var filter = new FilterRegistrationBean<HttpFilter>(new FakeSSOFilter(config.adminEntitlement(), config.developerEntitlement()));
|
||||
var filter = new FilterRegistrationBean<HttpFilter>(new FakeSSOFilter(config.developerEntitlement()));
|
||||
filter.setOrder(securityProperties.getFilter().getOrder() - 1);
|
||||
return filter;
|
||||
}
|
||||
|
@ -18,10 +18,12 @@ import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
|
||||
import org.springframework.security.web.util.RedirectUrlBuilder;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
@ -117,15 +119,21 @@ public class CustomAuthorizationEndpointFilter extends HttpFilter {
|
||||
Authentication authenticatedPrincipal = authenticationManager.authenticate(principal);
|
||||
|
||||
Authentication normalCodeRequest = authenticationConverter.convert(withGetMethod(request));
|
||||
Authentication codeRequest = overridePrincipal(authenticatedPrincipal, (OAuth2AuthorizationCodeRequestAuthenticationToken) normalCodeRequest);
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken codeRequestAuthenticationToken = (OAuth2AuthorizationCodeRequestAuthenticationToken) normalCodeRequest;
|
||||
Authentication codeRequest = overridePrincipal(authenticatedPrincipal, codeRequestAuthenticationToken);
|
||||
Authentication authenticatedCodeRequest = authenticationManager.authenticate(codeRequest);
|
||||
|
||||
if (!authenticatedCodeRequest.isAuthenticated()) {
|
||||
String authorizationUrl = getAuthorizationUrl(request);
|
||||
respondWithTemplate(response, templates.authorize(authorizationUrl, loggedInUser.getName(), (ShibbolethAuthenticationDetails) authenticatedPrincipal));
|
||||
} else {
|
||||
sendAuthorizationResponse(request, response, authenticatedCodeRequest);
|
||||
|
||||
if (authenticatedCodeRequest instanceof OAuth2AuthorizationCodeRequestAuthenticationToken authenticatedCodeRequestAuthenticationToken) {
|
||||
sendAuthorizationResponse(request, response, authenticatedCodeRequestAuthenticationToken);
|
||||
} else if (authenticatedCodeRequest instanceof OAuth2AuthorizationConsentAuthenticationToken consent) {
|
||||
sendConsentResponse(request, response, codeRequestAuthenticationToken, consent);
|
||||
} else {
|
||||
response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Invalid authentication token");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,10 +196,12 @@ public class CustomAuthorizationEndpointFilter extends HttpFilter {
|
||||
output.writeTo(response.getOutputStream());
|
||||
}
|
||||
|
||||
private void sendAuthorizationResponse(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication authentication) throws IOException {
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
|
||||
private void sendAuthorizationResponse(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication)
|
||||
throws IOException
|
||||
{
|
||||
UriComponentsBuilder uriBuilder = UriComponentsBuilder
|
||||
.fromUriString(authorizationCodeRequestAuthentication.getRedirectUri())
|
||||
.queryParam(OAuth2ParameterNames.CODE,
|
||||
@ -240,4 +250,31 @@ public class CustomAuthorizationEndpointFilter extends HttpFilter {
|
||||
String redirectUri = uriBuilder.build(true).toUriString();
|
||||
this.redirectStrategy.sendRedirect(request, response, redirectUri);
|
||||
}
|
||||
|
||||
private void sendConsentResponse(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken codeRequestAuthenticationToken,
|
||||
OAuth2AuthorizationConsentAuthenticationToken consent)
|
||||
throws IOException
|
||||
{
|
||||
Set<String> requestedScopes = codeRequestAuthenticationToken.getScopes();
|
||||
String consentUri = resolveConsentUri(request, "/oauth2/consent");
|
||||
String redirectUri = UriComponentsBuilder.fromUriString(consentUri)
|
||||
.queryParam(OAuth2ParameterNames.SCOPE, String.join(" ", requestedScopes))
|
||||
.queryParam(OAuth2ParameterNames.CLIENT_ID, consent.getClientId())
|
||||
.queryParam(OAuth2ParameterNames.STATE, consent.getState())
|
||||
.toUriString();
|
||||
this.redirectStrategy.sendRedirect(request, response, redirectUri);
|
||||
}
|
||||
|
||||
private String resolveConsentUri(HttpServletRequest request, String consentPage) {
|
||||
RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
|
||||
urlBuilder.setScheme(request.getScheme());
|
||||
urlBuilder.setServerName(request.getServerName());
|
||||
urlBuilder.setPort(request.getServerPort());
|
||||
urlBuilder.setContextPath(request.getContextPath());
|
||||
urlBuilder.setPathInfo(consentPage);
|
||||
return urlBuilder.getUrl();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
package se.su.dsv.oauth2.staging;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class SkipConsentForDevelopers implements Predicate<OAuth2AuthorizationCodeRequestAuthenticationContext> {
|
||||
private final Predicate<Authentication> developerAccessCheck;
|
||||
|
||||
public SkipConsentForDevelopers(final Predicate<Authentication> developerAccessCheck) {
|
||||
this.developerAccessCheck = developerAccessCheck;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(final OAuth2AuthorizationCodeRequestAuthenticationContext context) {
|
||||
Authentication currentUser = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (developerAccessCheck.test(currentUser)) {
|
||||
return false;
|
||||
} else {
|
||||
// TODO maybe check if consent is already given? useful to always request consent during testing?
|
||||
return context.getRegisteredClient().getClientSettings().isRequireAuthorizationConsent();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package se.su.dsv.oauth2.web.client;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@ -28,13 +29,16 @@ public class ClientAdminController {
|
||||
|
||||
private final ClientManagementService clientManagementService;
|
||||
private final AuthorizationServerSettings authorizationServerSettings;
|
||||
private final DeveloperAccessCheck developerAccessCheck;
|
||||
|
||||
public ClientAdminController(
|
||||
final ClientManagementService clientManagementService,
|
||||
AuthorizationServerSettings authorizationServerSettings)
|
||||
AuthorizationServerSettings authorizationServerSettings,
|
||||
DeveloperAccessCheck developerAccessCheck)
|
||||
{
|
||||
this.clientManagementService = clientManagementService;
|
||||
this.authorizationServerSettings = authorizationServerSettings;
|
||||
this.developerAccessCheck = developerAccessCheck;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@ -46,13 +50,13 @@ public class ClientAdminController {
|
||||
|
||||
@GetMapping("/new")
|
||||
public String newClient(Model model) {
|
||||
model.addAttribute("newClient", new NewClientRequest(null, null, null, null, null));
|
||||
model.addAttribute("newClient", NewClientRequest.empty());
|
||||
return "admin/client/new";
|
||||
}
|
||||
|
||||
@PostMapping("/new")
|
||||
public String createClient(
|
||||
Principal principal,
|
||||
Authentication principal,
|
||||
Model model,
|
||||
RedirectAttributes redirectAttributes,
|
||||
@ModelAttribute("newClient") NewClientRequest newClientRequest,
|
||||
@ -62,12 +66,14 @@ public class ClientAdminController {
|
||||
model.addAttribute("errors", bindingResult.getAllErrors());
|
||||
return "admin/client/new";
|
||||
}
|
||||
boolean requiresConsent = determineConsentRequired(principal, newClientRequest);
|
||||
ClientData clientData = new ClientData(
|
||||
newClientRequest.name(),
|
||||
newClientRequest.redirectUri(),
|
||||
newClientRequest.isPublic(),
|
||||
newClientRequest.scopes(),
|
||||
newClientRequest.contact());
|
||||
newClientRequest.contact(),
|
||||
requiresConsent);
|
||||
NewClient newClient = clientManagementService.createClient(principal, clientData);
|
||||
redirectAttributes.addFlashAttribute("message", "New client created");
|
||||
redirectAttributes.addFlashAttribute("clientId", newClient.clientId());
|
||||
@ -132,7 +138,7 @@ public class ClientAdminController {
|
||||
@PostMapping("/{id}/edit")
|
||||
public String editClient(
|
||||
@PathVariable("id") String id,
|
||||
Principal principal,
|
||||
Authentication principal,
|
||||
Model model,
|
||||
RedirectAttributes redirectAttributes,
|
||||
@ModelAttribute("newClient") NewClientRequest newClientRequest,
|
||||
@ -142,17 +148,29 @@ public class ClientAdminController {
|
||||
model.addAttribute("errors", bindingResult.getAllErrors());
|
||||
return "admin/client/edit";
|
||||
}
|
||||
boolean requiresConsent = determineConsentRequired(principal, newClientRequest);
|
||||
ClientData clientData = new ClientData(
|
||||
newClientRequest.name(),
|
||||
newClientRequest.redirectUri(),
|
||||
newClientRequest.isPublic(),
|
||||
newClientRequest.scopes(),
|
||||
newClientRequest.contact());
|
||||
newClientRequest.contact(),
|
||||
requiresConsent);
|
||||
final Client updatedClient = clientManagementService.updateClient(principal, id, clientData);
|
||||
redirectAttributes.addFlashAttribute("message", "Client updated");
|
||||
return "redirect:/admin/client/" + updatedClient.id();
|
||||
}
|
||||
|
||||
private boolean determineConsentRequired(final Authentication principal, final NewClientRequest newClientRequest) {
|
||||
if (hasDeveloperAccess(principal)) {
|
||||
// Allow developers to decide if consent is required
|
||||
return newClientRequest.requiresConsentBoolean();
|
||||
} else {
|
||||
// For everyone else it's always required
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public CsrfToken csrfToken(CsrfToken token) {
|
||||
return token;
|
||||
@ -162,4 +180,9 @@ public class ClientAdminController {
|
||||
public AuthorizationServerSettings authorizationServerSettings() {
|
||||
return authorizationServerSettings;
|
||||
}
|
||||
|
||||
@ModelAttribute(name = "developerAccess")
|
||||
public boolean hasDeveloperAccess(Authentication principal) {
|
||||
return developerAccessCheck.test(principal);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
package se.su.dsv.oauth2.web.client;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DeveloperAccessCheck extends Predicate<Authentication> {
|
||||
}
|
@ -14,7 +14,8 @@ public record NewClientRequest(
|
||||
String contact,
|
||||
URI redirectUri,
|
||||
String public_,
|
||||
String scope)
|
||||
String scope,
|
||||
String requiresConsent)
|
||||
{
|
||||
public static NewClientRequest from(final Client client) {
|
||||
return new NewClientRequest(
|
||||
@ -22,7 +23,12 @@ public record NewClientRequest(
|
||||
client.contact(),
|
||||
URI.create(client.redirectUri()),
|
||||
client.isPublic() ? "on" : null,
|
||||
String.join("\r\n", client.scopes()));
|
||||
String.join("\r\n", client.scopes()),
|
||||
client.requiresConsent() ? "on" : null);
|
||||
}
|
||||
|
||||
public static NewClientRequest empty() {
|
||||
return new NewClientRequest(null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public boolean isPublic() {
|
||||
@ -40,4 +46,8 @@ public record NewClientRequest(
|
||||
public String redirectUriString() {
|
||||
return redirectUri == null ? "" : redirectUri.toString();
|
||||
}
|
||||
|
||||
public boolean requiresConsentBoolean() {
|
||||
return "on".equals(requiresConsent);
|
||||
}
|
||||
}
|
||||
|
150
src/main/java/se/su/dsv/oauth2/web/oauth2/ConsentController.java
Normal file
150
src/main/java/se/su/dsv/oauth2/web/oauth2/ConsentController.java
Normal file
@ -0,0 +1,150 @@
|
||||
package se.su.dsv.oauth2.web.oauth2;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
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.OAuth2TokenType;
|
||||
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.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.ErrorResponseException;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import se.su.dsv.oauth2.shibboleth.ShibbolethAuthenticationDetails;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Controller
|
||||
public class ConsentController {
|
||||
|
||||
private final AuthorizationServerSettings authorizationServerSettings;
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
|
||||
public ConsentController(
|
||||
final AuthorizationServerSettings authorizationServerSettings,
|
||||
final RegisteredClientRepository registeredClientRepository,
|
||||
final OAuth2AuthorizationService authorizationService)
|
||||
{
|
||||
this.authorizationServerSettings = authorizationServerSettings;
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
@GetMapping("/oauth2/consent")
|
||||
public String showConsentForm(
|
||||
Authentication authentication,
|
||||
Model model,
|
||||
UriComponentsBuilder uriComponentsBuilder,
|
||||
@RequestParam("scope") String scopeString,
|
||||
@RequestParam("client_id") String clientId,
|
||||
@RequestParam("state") String state)
|
||||
{
|
||||
RegisteredClient client = registeredClientRepository.findByClientId(clientId);
|
||||
if (client == null) {
|
||||
throw new ErrorResponseException(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
model.addAttribute("clientName", client.getClientName());
|
||||
model.addAttribute("clientDomain", getClientDomain(client));
|
||||
|
||||
String authorizationUrl = uriComponentsBuilder
|
||||
.path(authorizationServerSettings.getAuthorizationEndpoint())
|
||||
.toUriString();
|
||||
model.addAttribute("authorizationUrl", authorizationUrl);
|
||||
|
||||
Set<String> scopes = getRequestScopes(scopeString);
|
||||
model.addAttribute("scopes", scopes);
|
||||
|
||||
model.addAttribute("clientId", clientId);
|
||||
model.addAttribute("state", state);
|
||||
|
||||
PersonalInformation personalInformation = getPersonalInformation(authentication);
|
||||
model.addAttribute("personalInformation", personalInformation);
|
||||
|
||||
return "consent";
|
||||
}
|
||||
|
||||
private PersonalInformation getPersonalInformation(Authentication authentication) {
|
||||
if (authentication.getDetails() instanceof ShibbolethAuthenticationDetails details) {
|
||||
return new PersonalInformation(
|
||||
details.givenName(),
|
||||
details.familyName(),
|
||||
details.displayName(),
|
||||
details.emailAddress());
|
||||
}
|
||||
else {
|
||||
return new PersonalInformation(null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/oauth2/consent")
|
||||
public String denyConsent(@RequestParam("state") String state) {
|
||||
OAuth2Authorization authorization = authorizationService.findByToken(
|
||||
state,
|
||||
new OAuth2TokenType(OAuth2ParameterNames.STATE));
|
||||
if (authorization == null) {
|
||||
return "redirect:/";
|
||||
}
|
||||
else {
|
||||
String registeredClientId = authorization.getRegisteredClientId();
|
||||
RegisteredClient registeredClient = registeredClientRepository.findById(registeredClientId);
|
||||
authorizationService.remove(authorization);
|
||||
if (registeredClient == null) {
|
||||
return "redirect:/";
|
||||
}
|
||||
String redirectUri = registeredClient.getRedirectUris()
|
||||
.stream()
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ErrorResponseException(HttpStatus.BAD_REQUEST));
|
||||
return "redirect:" + redirectUri +
|
||||
"?error=" + OAuth2ErrorCodes.ACCESS_DENIED +
|
||||
"&state=" + state;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getClientDomain(final RegisteredClient client) {
|
||||
return client.getRedirectUris()
|
||||
.stream()
|
||||
.map(UriComponentsBuilder::fromUriString)
|
||||
.map(UriComponentsBuilder::build)
|
||||
.map(uri -> {
|
||||
String scheme = uri.getScheme();
|
||||
String host = uri.getHost();
|
||||
return scheme + "://" + host;
|
||||
})
|
||||
.findAny()
|
||||
.orElseThrow(() -> new ErrorResponseException(HttpStatus.BAD_REQUEST));
|
||||
}
|
||||
|
||||
private static Set<String> getRequestScopes(final String scopeString) {
|
||||
if (scopeString == null) {
|
||||
return Set.of();
|
||||
}
|
||||
return Arrays.stream(scopeString.split(" "))
|
||||
.filter(s -> !s.isBlank())
|
||||
.filter(scope -> !scope.equals("openid"))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@ModelAttribute("currentUser")
|
||||
public Authentication authentication(Authentication authentication) {
|
||||
return authentication;
|
||||
}
|
||||
|
||||
@ModelAttribute("csrfToken")
|
||||
public CsrfToken csrfToken(CsrfToken csrfToken) {
|
||||
return csrfToken;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package se.su.dsv.oauth2.web.oauth2;
|
||||
|
||||
public record PersonalInformation(
|
||||
String givenName,
|
||||
String familyName,
|
||||
String displayName,
|
||||
String email)
|
||||
{
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
se.su.dsv.oauth2.admin-entitlement=oauth2-admin
|
||||
se.su.dsv.oauth2.developer-entitlement=oauth2-developer
|
||||
gg.jte.templateLocation=src/main/resources/templates
|
||||
gg.jte.developmentMode=true
|
||||
|
@ -0,0 +1,6 @@
|
||||
ALTER TABLE `v2_client`
|
||||
# Defaulting user consent to true so that future clients will require user consent
|
||||
ADD COLUMN `requires_consent` BOOLEAN NOT NULL DEFAULT TRUE;
|
||||
|
||||
# Making sure current clients stay the same
|
||||
UPDATE `v2_client` SET `requires_consent` = FALSE;
|
7
src/main/resources/db/migration/V6__user_consent.sql
Normal file
7
src/main/resources/db/migration/V6__user_consent.sql
Normal file
@ -0,0 +1,7 @@
|
||||
CREATE TABLE oauth2_authorization_consent
|
||||
(
|
||||
registered_client_id varchar(100) NOT NULL,
|
||||
principal_name varchar(200) NOT NULL,
|
||||
authorities varchar(1000) NOT NULL,
|
||||
PRIMARY KEY (registered_client_id, principal_name)
|
||||
);
|
@ -7,6 +7,7 @@
|
||||
@param Client client
|
||||
@param String feedback
|
||||
@param List<ObjectError> errors
|
||||
@param boolean developerAccess
|
||||
|
||||
@template.base(title = "Edit client " + client.name(), content = @`
|
||||
<nav>
|
||||
@ -50,6 +51,16 @@
|
||||
<label class="form-label" for="redirectUri">Redirect URI</label>
|
||||
<input class="form-control" name="redirectUri" id="redirectUri" type="url" value="${newClient.redirectUriString()}">
|
||||
</div>
|
||||
@if (developerAccess)
|
||||
<div class="mb-3 form-check">
|
||||
<input class="form-check-input" type="checkbox" id="consent" name="requiresConsent" checked="${newClient.requiresConsentBoolean()}">
|
||||
<label class="form-check-label" for="consent">Require user consent</label>
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
If checked, the user will be asked to consent to grant access to this client.
|
||||
</small>
|
||||
</div>
|
||||
@endif
|
||||
<div class="mb-3 form-check">
|
||||
<input class="form-check-input" type="checkbox" id="public" name="public_" checked="${newClient.isPublic()}">
|
||||
<label class="form-check-label" for="public">Public client</label>
|
||||
|
@ -5,6 +5,7 @@
|
||||
@param se.su.dsv.oauth2.web.client.NewClientRequest newClient
|
||||
@param String feedback
|
||||
@param List<ObjectError> errors
|
||||
@param boolean developerAccess
|
||||
|
||||
@template.base(title = "Create new client", content = @`
|
||||
<nav>
|
||||
@ -59,6 +60,16 @@
|
||||
<label class="form-label" for="redirectUri">Redirect URI</label>
|
||||
<input class="form-control" name="redirectUri" id="redirectUri" type="url" value="${newClient.redirectUriString()}">
|
||||
</div>
|
||||
@if (developerAccess)
|
||||
<div class="mb-3 form-check">
|
||||
<input class="form-check-input" type="checkbox" id="consent" name="requiresConsent" checked="${newClient.requiresConsentBoolean()}">
|
||||
<label class="form-check-label" for="consent">Require user consent</label>
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
If checked, the user will be asked to consent to grant access to this client.
|
||||
</small>
|
||||
</div>
|
||||
@endif
|
||||
<div class="mb-3 form-check">
|
||||
<input class="form-check-input" type="checkbox" id="public" name="public_" checked="${newClient.isPublic()}">
|
||||
<label class="form-check-label" for="public">Public client</label>
|
||||
|
@ -67,6 +67,8 @@
|
||||
<dt>Redirect URI</dt>
|
||||
<dd>${client.redirectUri()}</dd>
|
||||
@endif
|
||||
<dt>Requires consent</dt>
|
||||
<dd>${client.requiresConsent() ? "Yes" : "No"}</dd>
|
||||
<dt>Public client</dt>
|
||||
<dd>${client.isPublic() ? "Yes" : "No"}</dd>
|
||||
<dt>Scopes</dt>
|
||||
|
46
src/main/resources/templates/consent.jte
Normal file
46
src/main/resources/templates/consent.jte
Normal file
@ -0,0 +1,46 @@
|
||||
@import org.springframework.security.core.Authentication
|
||||
@import org.springframework.security.web.csrf.CsrfToken
|
||||
@import se.su.dsv.oauth2.web.oauth2.PersonalInformation
|
||||
@import java.util.Set
|
||||
|
||||
@param String clientId
|
||||
@param String clientName
|
||||
@param String clientDomain
|
||||
@param String authorizationUrl
|
||||
@param String state
|
||||
@param Authentication currentUser
|
||||
@param Set<String> scopes
|
||||
@param CsrfToken csrfToken
|
||||
@param PersonalInformation personalInformation
|
||||
|
||||
@template.base(title = "Consent", content = @`
|
||||
<h1>Consent</h1>
|
||||
|
||||
<p>
|
||||
The application <strong>${clientName}</strong> located at
|
||||
<strong>${clientDomain}</strong> wants to access your information.
|
||||
</p>
|
||||
<p>
|
||||
See below for what information it is requesting and select what information
|
||||
you're willing to share.
|
||||
</p>
|
||||
<form method="post" action="${authorizationUrl}">
|
||||
<input type="hidden" name="client_id" value="${clientId}">
|
||||
<input type="hidden" name="state" value="${state}">
|
||||
<input type="hidden" name="${csrfToken.getParameterName()}" value="${csrfToken.getToken()}">
|
||||
|
||||
<ul class="list-group mb-3">
|
||||
<li class="list-group-item">
|
||||
Account name: ${currentUser.getName()}
|
||||
</li>
|
||||
@for (var scope : scopes)
|
||||
<li class="list-group-item">
|
||||
@template.consent_scope(scope = scope, personalInformation = personalInformation)
|
||||
</li>
|
||||
@endfor
|
||||
</ul>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Consent to sharing the above information</button>
|
||||
<button type="submit" formaction="/oauth2/consent" class="btn btn-link">Deny</button>
|
||||
</form>
|
||||
`)
|
33
src/main/resources/templates/consent_scope.jte
Normal file
33
src/main/resources/templates/consent_scope.jte
Normal file
@ -0,0 +1,33 @@
|
||||
@import se.su.dsv.oauth2.web.oauth2.PersonalInformation
|
||||
@import java.util.Objects
|
||||
|
||||
@param String scope
|
||||
@param PersonalInformation personalInformation
|
||||
|
||||
<label class="d-flex gap-3">
|
||||
<input class="form-check-input flex-shrink-0" type="checkbox" name="scope" value="${scope}" id="scope_${scope}" checked aria-label="${scope}">
|
||||
@if (Objects.equals("profile", scope))
|
||||
<dl>
|
||||
<dt>Given name</dt>
|
||||
<dd>${personalInformation.givenName()}</dd>
|
||||
|
||||
<dt>Family name</dt>
|
||||
<dd>${personalInformation.familyName()}</dd>
|
||||
|
||||
<dt>Display name</dt>
|
||||
<dd>${personalInformation.displayName()}</dd>
|
||||
</dl>
|
||||
@elseif (Objects.equals("email", scope))
|
||||
<dl>
|
||||
<dt>E-mail address</dt>
|
||||
<dd>${personalInformation.email()}</dd>
|
||||
</dl>
|
||||
@elseif (Objects.equals("offline_access", scope))
|
||||
<div>Maintain access after you leave the application</div>
|
||||
@else
|
||||
<div>${scope}</div>
|
||||
@endif
|
||||
<div class="ms-auto">
|
||||
<small class="text-muted">scope: ${scope}</small>
|
||||
</div>
|
||||
</label>
|
@ -0,0 +1,104 @@
|
||||
package se.su.dsv.oauth2;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.testcontainers.containers.MariaDBContainer;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
import static se.su.dsv.oauth2.ShibbolethRequestProcessor.remoteUser;
|
||||
|
||||
@SpringBootTest(
|
||||
properties = {
|
||||
"se.su.dsv.oauth2.developer-entitlement=" + ConsentFlowCustomAuthorizationTest.DEVELOPER_ENTITLEMENT
|
||||
},
|
||||
classes = ConsentFlowTest.TestConfig.class)
|
||||
@ActiveProfiles("staging")
|
||||
public class ConsentFlowCustomAuthorizationTest extends AbstractMetadataTest {
|
||||
public static final String DEVELOPER_ENTITLEMENT = "developer";
|
||||
|
||||
@ServiceConnection
|
||||
static MariaDBContainer<?> mariaDBContainer = new MariaDBContainer<>("mariadb:10.11");
|
||||
|
||||
@Test
|
||||
public void consent_is_disabled_for_developers() throws Exception {
|
||||
String developerPrincipal = "master-of-the-universe";
|
||||
String customPrincipal = "lowly-regular-user";
|
||||
|
||||
MvcResult authorizationResult = mockMvc.perform(post(getAuthorizationEndpoint())
|
||||
.with(remoteUser(developerPrincipal)
|
||||
.entitlement(DEVELOPER_ENTITLEMENT))
|
||||
.queryParam("client_id", ConsentFlowTest.TestConfig.CLIENT_ID)
|
||||
.queryParam("response_type", "code")
|
||||
.formField("principal", customPrincipal))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrlPattern(ConsentFlowTest.TestConfig.REDIRECT_URI + "?*"))
|
||||
.andReturn();
|
||||
|
||||
String redirectUrl = getRedirectUrl(authorizationResult);
|
||||
String authorizationCode = UriComponentsBuilder.fromUriString(redirectUrl)
|
||||
.build()
|
||||
.getQueryParams()
|
||||
.getFirst("code");
|
||||
|
||||
assertNotNull(authorizationCode, "Should have received an authorization code");
|
||||
|
||||
MvcResult tokenResult = mockMvc.perform(post(getTokenEndpoint())
|
||||
.with(httpBasic(ConsentFlowTest.TestConfig.CLIENT_ID, ConsentFlowTest.TestConfig.CLIENT_SECRET))
|
||||
.formField("grant_type", "authorization_code")
|
||||
.formField("code", authorizationCode))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").exists())
|
||||
.andReturn();
|
||||
|
||||
JsonNode tokenResponse = objectMapper.readTree(tokenResult.getResponse().getContentAsString());
|
||||
JWTClaimsSet claims = verifyToken(tokenResponse.required("access_token").asText());
|
||||
|
||||
assertEquals(customPrincipal, claims.getSubject());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regular_users_are_prompted_for_consent() throws Exception {
|
||||
String regularPrincipal = "lowly-regular-user";
|
||||
|
||||
MvcResult authorizationResult = mockMvc.perform(get(getAuthorizationEndpoint())
|
||||
.with(remoteUser(regularPrincipal))
|
||||
.queryParam("client_id", ConsentFlowTest.TestConfig.CLIENT_ID)
|
||||
.queryParam("response_type", "code"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrlPattern("**/oauth2/consent?*"))
|
||||
.andReturn();
|
||||
|
||||
String redirectUrl = getRedirectUrl(authorizationResult);
|
||||
MultiValueMap<String, String> queryParams = UriComponentsBuilder.fromUriString(redirectUrl)
|
||||
.build()
|
||||
.getQueryParams();
|
||||
|
||||
mockMvc.perform(post(getAuthorizationEndpoint())
|
||||
.with(remoteUser(regularPrincipal))
|
||||
.formField("client_id", queryParams.getFirst("client_id"))
|
||||
.formField("state", queryParams.getFirst("state")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrlPattern(ConsentFlowTest.TestConfig.REDIRECT_URI + "?*"));
|
||||
}
|
||||
|
||||
private static String getRedirectUrl(final MvcResult result) {
|
||||
String redirectedUrl = result.getResponse().getRedirectedUrl();
|
||||
assertNotNull(redirectedUrl, "Should have gotten a redirect URL");
|
||||
return URLDecoder.decode(redirectedUrl, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
204
src/test/java/se/su/dsv/oauth2/ConsentFlowTest.java
Normal file
204
src/test/java/se/su/dsv/oauth2/ConsentFlowTest.java
Normal file
@ -0,0 +1,204 @@
|
||||
package se.su.dsv.oauth2;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.testcontainers.containers.MariaDBContainer;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
import static se.su.dsv.oauth2.ShibbolethRequestProcessor.remoteUser;
|
||||
|
||||
@SpringBootTest
|
||||
public class ConsentFlowTest extends AbstractMetadataTest {
|
||||
|
||||
@ServiceConnection
|
||||
static MariaDBContainer<?> mariaDBContainer = new MariaDBContainer<>("mariadb:10.11");
|
||||
|
||||
@Test
|
||||
public void asks_end_user_for_consent() throws Exception {
|
||||
MvcResult authorizeResult = mockMvc.perform(get(getAuthorizationEndpoint())
|
||||
.with(remoteUser("some-end-user"))
|
||||
.queryParam("response_type", "code")
|
||||
.queryParam("client_id", TestConfig.CLIENT_ID)
|
||||
.queryParam("redirect_uri", TestConfig.REDIRECT_URI))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrlPattern("**/oauth2/consent?**"))
|
||||
.andReturn();
|
||||
|
||||
UriComponents redirectUrl = parseUrl(authorizeResult.getResponse().getRedirectedUrl());
|
||||
String state = redirectUrl.getQueryParams().getFirst("state");
|
||||
String clientId = redirectUrl.getQueryParams().getFirst("client_id");
|
||||
|
||||
MvcResult consentResult = mockMvc.perform(post(getAuthorizationEndpoint())
|
||||
.with(remoteUser("some-end-user"))
|
||||
.formField("client_id", clientId)
|
||||
.formField("state", state))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrlPattern(TestConfig.REDIRECT_URI + "?**"))
|
||||
.andReturn();
|
||||
|
||||
UriComponents callbackUrl = parseUrl(consentResult.getResponse().getRedirectedUrl());
|
||||
|
||||
String code = callbackUrl.getQueryParams().getFirst("code");
|
||||
assertNotNull(code, "Should have received a one time authorization code");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void consent_page_exists() throws Exception {
|
||||
attemptAuthorizationWithConsentResponse("some-other-end-user")
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void consent_page_displays_information_so_end_user_can_make_informed_decision() throws Exception {
|
||||
String principal = "some-other-end-user";
|
||||
|
||||
attemptAuthorizationWithConsentResponse(principal)
|
||||
.andExpect(status().isOk())
|
||||
.andExpectAll(
|
||||
content().string(containsString(TestConfig.CLIENT_NAME)),
|
||||
content().string(containsString(TestConfig.CLIENT_DOMAIN)),
|
||||
content().string(containsString(principal)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shows_requested_scopes() throws Exception {
|
||||
attemptAuthorizationWithConsentResponseUsingScopes("some-other-end-user", Set.of("openid", "email", "profile"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpectAll(
|
||||
content().string(containsString("email")),
|
||||
content().string(containsString("profile")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void does_not_ask_for_consent_for_openid_scope() throws Exception {
|
||||
attemptAuthorizationWithConsentResponseUsingScopes("some-other-end-user", Set.of("openid", "profile"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(not(containsString("openid"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deny_consent_redirects_back_to_client_with_access_denied_error() throws Exception {
|
||||
String principal = "some-other-end-user";
|
||||
MvcResult consentResponse = attemptAuthorizationWithConsentResponse(principal)
|
||||
.andReturn();
|
||||
|
||||
String consentURL = consentResponse.getRequest()
|
||||
.getRequestURL()
|
||||
.append("?")
|
||||
.append(consentResponse.getRequest().getQueryString())
|
||||
.toString();
|
||||
|
||||
UriComponents consentUri = parseUrl(consentURL);
|
||||
|
||||
String clientId = consentUri.getQueryParams().getFirst("client_id");
|
||||
String state = consentUri.getQueryParams().getFirst("state");
|
||||
|
||||
MvcResult denyResult = mockMvc.perform(post("/oauth2/consent")
|
||||
.with(remoteUser(principal))
|
||||
.with(csrf())
|
||||
.formField("client_id", clientId)
|
||||
.formField("state", state))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
|
||||
UriComponents redirectedUri = parseUrl(denyResult.getResponse().getRedirectedUrl());
|
||||
|
||||
UriComponents clientRedirectUri = parseUrl(TestConfig.REDIRECT_URI);
|
||||
|
||||
assertEquals(clientRedirectUri.getHost(), redirectedUri.getHost());
|
||||
assertEquals(clientRedirectUri.getScheme(), redirectedUri.getScheme());
|
||||
assertEquals(clientRedirectUri.getPath(), redirectedUri.getPath());
|
||||
assertEquals("access_denied", redirectedUri.getQueryParams().getFirst("error"));
|
||||
}
|
||||
|
||||
private ResultActions attemptAuthorizationWithConsentResponse(String principal) throws Exception {
|
||||
Set<String> scopes = Set.of();
|
||||
return attemptAuthorizationWithConsentResponseUsingScopes(principal, scopes);
|
||||
}
|
||||
|
||||
private ResultActions attemptAuthorizationWithConsentResponseUsingScopes(String principal, Set<String> scopes)
|
||||
throws Exception
|
||||
{
|
||||
MvcResult result = mockMvc.perform(get(getAuthorizationEndpoint())
|
||||
.with(remoteUser(principal))
|
||||
.queryParam("response_type", "code")
|
||||
.queryParam("client_id", TestConfig.CLIENT_ID)
|
||||
.queryParam("redirect_uri", TestConfig.REDIRECT_URI)
|
||||
.queryParam("scope", String.join(" ", scopes)))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrlPattern("**/oauth2/consent?**"))
|
||||
.andReturn();
|
||||
|
||||
String redirectedUrl = result.getResponse().getRedirectedUrl();
|
||||
assertNotNull(redirectedUrl, "Should have redirected to the consent page");
|
||||
String consentUrl = URLDecoder.decode(redirectedUrl, StandardCharsets.UTF_8);
|
||||
|
||||
return mockMvc.perform(get(consentUrl)
|
||||
.with(remoteUser(principal)));
|
||||
}
|
||||
|
||||
private static UriComponents parseUrl(final String url) {
|
||||
assertNotNull(url);
|
||||
|
||||
String decodedUrl = URLDecoder.decode(url, StandardCharsets.UTF_8);
|
||||
return UriComponentsBuilder
|
||||
.fromUriString(decodedUrl)
|
||||
.build();
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
public static class TestConfig {
|
||||
public static final String CLIENT_ID = "client";
|
||||
public static final String CLIENT_SECRET = "secret";
|
||||
public static final String CLIENT_DOMAIN = "http://localhost";
|
||||
public static final String REDIRECT_URI = CLIENT_DOMAIN + "/login/oauth2/code/client";
|
||||
public static final String CLIENT_NAME = "My test client";
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
// Required to override the default RegisteredClientRepository
|
||||
InMemoryRegisteredClientRepository testRegisteredClientRepository()
|
||||
{
|
||||
RegisteredClient registeredClient = RegisteredClient.withId("id")
|
||||
.clientId(CLIENT_ID)
|
||||
.clientName(CLIENT_NAME)
|
||||
.clientSecret("{noop}" + CLIENT_SECRET)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.redirectUri(REDIRECT_URI)
|
||||
.scope(OidcScopes.OPENID)
|
||||
.scope(OidcScopes.PROFILE)
|
||||
.scope(OidcScopes.EMAIL)
|
||||
.clientSettings(ClientSettings.builder()
|
||||
.requireAuthorizationConsent(true)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
return new InMemoryRegisteredClientRepository(registeredClient);
|
||||
}
|
||||
}
|
||||
}
|
@ -42,7 +42,7 @@ public class PublicClientCodeFlowTest extends AbstractMetadataCodeFlowTest {
|
||||
public void register_public_client() {
|
||||
Set<String> scopes = Set.of("openid", "offline_access");
|
||||
ClientData clientData = new ClientData("public-client", URI.create(REDIRECT_URI), true,
|
||||
scopes, "admin@localhost");
|
||||
scopes, "admin@localhost", false);
|
||||
newClient = clientManagementService.createClient(() -> "admin", clientData);
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ public class ResourceServerRegisteredClientTest extends AbstractMetadataCodeFlow
|
||||
|
||||
@Test
|
||||
public void client_authenticates_then_resource_server_introspects() throws Exception {
|
||||
ClientData application = new ClientData("client", URI.create("http://myapplication.local/oauth2"), false, Set.of(), "admin@localhost");
|
||||
ClientData application = new ClientData("client", URI.create("http://myapplication.local/oauth2"), false, Set.of(), "admin@localhost", false);
|
||||
ClientData resourceServer = new ClientData("resource-server", "admin@localhost");
|
||||
NewClient application1 = clientManagementService.createClient(() -> "admin", application);
|
||||
NewClient resourceServer1 = clientManagementService.createClient(() -> "admin", resourceServer);
|
||||
|
@ -9,22 +9,15 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.testcontainers.containers.MariaDBContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import se.su.dsv.oauth2.shibboleth.Entitlement;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@SpringBootTest(
|
||||
properties = {
|
||||
"se.su.dsv.oauth2.admin-entitlement=" + AdminControllerTest.ADMIN_ENTITLEMENT
|
||||
}
|
||||
)
|
||||
@SpringBootTest
|
||||
@Testcontainers
|
||||
@AutoConfigureMockMvc
|
||||
class AdminControllerTest {
|
||||
static final String ADMIN_ENTITLEMENT = "ADMIN";
|
||||
|
||||
@Container
|
||||
@ServiceConnection
|
||||
static MariaDBContainer<?> mariaDBContainer = new MariaDBContainer<>("mariadb:10.11");
|
||||
@ -39,9 +32,9 @@ class AdminControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void is_accessible_with_admin_authority() throws Exception {
|
||||
void is_accessible_when_logged_in() throws Exception {
|
||||
mockMvc.perform(get("/admin")
|
||||
.with(user("admin").authorities(new Entitlement(ADMIN_ENTITLEMENT))))
|
||||
.with(user("admin")))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,18 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.springframework.test.annotation.Rollback;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.testcontainers.containers.MariaDBContainer;
|
||||
import se.su.dsv.oauth2.admin.Client;
|
||||
import se.su.dsv.oauth2.admin.ClientManagementService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
@ -20,11 +26,13 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
import static se.su.dsv.oauth2.ShibbolethRequestProcessor.remoteUser;
|
||||
|
||||
@SpringBootTest(properties = {
|
||||
"se.su.dsv.oauth2." + ClientAdminControllerTest.ADMIN_ENTITLEMENT + "-entitlement=admin"
|
||||
"se.su.dsv.oauth2.developer-entitlement=" + ClientAdminControllerTest.DEVELOPER_ENTITLEMENT
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
@Transactional
|
||||
@Rollback
|
||||
public class ClientAdminControllerTest {
|
||||
public static final String ADMIN_ENTITLEMENT = "admin";
|
||||
public static final String DEVELOPER_ENTITLEMENT = "developer";
|
||||
|
||||
@ServiceConnection
|
||||
static MariaDBContainer<?> mariaDBContainer = new MariaDBContainer<>("mariadb:10.11");
|
||||
@ -32,6 +40,9 @@ public class ClientAdminControllerTest {
|
||||
@Autowired
|
||||
MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
ClientManagementService clientManagementService;
|
||||
|
||||
@Test
|
||||
public void register_new_client() throws Exception {
|
||||
String name = "My client";
|
||||
@ -41,8 +52,7 @@ public class ClientAdminControllerTest {
|
||||
|
||||
MvcResult creationResult = mockMvc.perform(post("/admin/client/new")
|
||||
.with(csrf())
|
||||
.with(remoteUser("admin")
|
||||
.entitlement(ADMIN_ENTITLEMENT))
|
||||
.with(remoteUser("admin"))
|
||||
.formField("name", name)
|
||||
.formField("contact", contactEmail)
|
||||
.formField("redirectUri", redirectUri)
|
||||
@ -55,11 +65,65 @@ public class ClientAdminControllerTest {
|
||||
assertNotNull(viewClientUrl);
|
||||
|
||||
mockMvc.perform(get(viewClientUrl)
|
||||
.with(remoteUser("admin")
|
||||
.entitlement(ADMIN_ENTITLEMENT)))
|
||||
.with(remoteUser("admin")))
|
||||
.andExpect(content().string(containsString(name)))
|
||||
.andExpect(content().string(containsString(contactEmail)))
|
||||
.andExpect(content().string(containsString(redirectUri)))
|
||||
.andExpect(content().string(stringContainsInOrder(scopes)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void register_client_without_developer_access_always_require_consent() throws Exception {
|
||||
String name = "My client";
|
||||
String contactEmail = DEVELOPER_ENTITLEMENT + "@3rd-party.example";
|
||||
String redirectUri = "https://3rd-party.example/oauth2/callback";
|
||||
String principal = "third-party-" + DEVELOPER_ENTITLEMENT;
|
||||
|
||||
MvcResult creationResult = mockMvc.perform(post("/admin/client/new")
|
||||
.with(csrf())
|
||||
.with(remoteUser(principal))
|
||||
.formField("name", name)
|
||||
.formField("contact", contactEmail)
|
||||
.formField("redirectUri", redirectUri))
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(flash().attribute("clientSecret", not(blankOrNullString())))
|
||||
.andReturn();
|
||||
|
||||
String viewClientUrl = creationResult.getResponse().getRedirectedUrl();
|
||||
assertNotNull(viewClientUrl);
|
||||
|
||||
List<Client> clients = clientManagementService.getClients(() -> principal);
|
||||
assertThat(clients, hasSize(1));
|
||||
|
||||
Client client = clients.get(0);
|
||||
assertThat(client.requiresConsent(), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void register_client_with_developer_access_can_bypass_consent() throws Exception {
|
||||
String name = "My client";
|
||||
String contactEmail = DEVELOPER_ENTITLEMENT + "@3rd-party.example";
|
||||
String redirectUri = "https://3rd-party.example/oauth2/callback";
|
||||
String principal = "third-party-" + DEVELOPER_ENTITLEMENT;
|
||||
|
||||
MvcResult creationResult = mockMvc.perform(post("/admin/client/new")
|
||||
.with(csrf())
|
||||
.with(remoteUser(principal)
|
||||
.entitlement(DEVELOPER_ENTITLEMENT))
|
||||
.formField("name", name)
|
||||
.formField("contact", contactEmail)
|
||||
.formField("redirectUri", redirectUri))
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(flash().attribute("clientSecret", not(blankOrNullString())))
|
||||
.andReturn();
|
||||
|
||||
String viewClientUrl = creationResult.getResponse().getRedirectedUrl();
|
||||
assertNotNull(viewClientUrl);
|
||||
|
||||
List<Client> clients = clientManagementService.getClients(() -> principal);
|
||||
assertThat(clients, hasSize(1));
|
||||
|
||||
Client client = clients.get(0);
|
||||
assertThat(client.requiresConsent(), is(false));
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user