Implement support for user consent #4

Manually merged
ansv7779 merged 13 commits from user-consent into main 2025-04-25 10:22:44 +02:00
4 changed files with 88 additions and 1 deletions
Showing only changes of commit 31cd05b12e - Show all commits

View File

@ -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<>();

View File

@ -2,14 +2,21 @@ 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;
@ -22,13 +29,16 @@ public class ConsentController {
private final AuthorizationServerSettings authorizationServerSettings;
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
public ConsentController(
final AuthorizationServerSettings authorizationServerSettings,
final RegisteredClientRepository registeredClientRepository)
final RegisteredClientRepository registeredClientRepository,
final OAuth2AuthorizationService authorizationService)
{
this.authorizationServerSettings = authorizationServerSettings;
this.registeredClientRepository = registeredClientRepository;
this.authorizationService = authorizationService;
}
@GetMapping("/oauth2/consent")
@ -61,6 +71,31 @@ public class ConsentController {
return "consent";
}
@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()
@ -89,4 +124,9 @@ public class ConsentController {
public Authentication authentication(Authentication authentication) {
return authentication;
}
@ModelAttribute("csrfToken")
public CsrfToken csrfToken(CsrfToken csrfToken) {
return csrfToken;
}
}

View File

@ -1,4 +1,5 @@
@import org.springframework.security.core.Authentication
@import org.springframework.security.web.csrf.CsrfToken
@import java.util.Set
@param String clientId
@ -8,6 +9,7 @@
@param String state
@param Authentication currentUser
@param Set<String> scopes
@param CsrfToken csrfToken
@template.base(title = "Consent", content = @`
<h1>Consent</h1>
@ -23,6 +25,7 @@
<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">

View File

@ -23,7 +23,9 @@ 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.*;
@ -99,6 +101,41 @@ public class ConsentFlowTest extends AbstractMetadataTest {
.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);