Implement support for user consent #4
@ -3,6 +3,8 @@ package se.su.dsv.oauth2;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Profile;
|
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.ClientRepository;
|
||||||
import se.su.dsv.oauth2.admin.repository.ClientRow;
|
import se.su.dsv.oauth2.admin.repository.ClientRow;
|
||||||
|
|
||||||
@ -15,6 +17,11 @@ import java.util.Optional;
|
|||||||
@Configuration
|
@Configuration
|
||||||
@Profile("embedded")
|
@Profile("embedded")
|
||||||
public class EmbeddedConfiguration {
|
public class EmbeddedConfiguration {
|
||||||
|
@Bean
|
||||||
|
public OAuth2AuthorizationService authorizationService() {
|
||||||
|
return new InMemoryOAuth2AuthorizationService();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ClientRepository clientRepository() {
|
public ClientRepository clientRepository() {
|
||||||
ArrayList<ClientRow> clients = new ArrayList<>();
|
ArrayList<ClientRow> clients = new ArrayList<>();
|
||||||
|
|||||||
@ -2,14 +2,21 @@ package se.su.dsv.oauth2.web.oauth2;
|
|||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.core.Authentication;
|
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.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.settings.AuthorizationServerSettings;
|
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||||
|
import org.springframework.security.web.csrf.CsrfToken;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.ErrorResponseException;
|
import org.springframework.web.ErrorResponseException;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
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.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
@ -22,13 +29,16 @@ public class ConsentController {
|
|||||||
|
|
||||||
private final AuthorizationServerSettings authorizationServerSettings;
|
private final AuthorizationServerSettings authorizationServerSettings;
|
||||||
private final RegisteredClientRepository registeredClientRepository;
|
private final RegisteredClientRepository registeredClientRepository;
|
||||||
|
private final OAuth2AuthorizationService authorizationService;
|
||||||
|
|
||||||
public ConsentController(
|
public ConsentController(
|
||||||
final AuthorizationServerSettings authorizationServerSettings,
|
final AuthorizationServerSettings authorizationServerSettings,
|
||||||
final RegisteredClientRepository registeredClientRepository)
|
final RegisteredClientRepository registeredClientRepository,
|
||||||
|
final OAuth2AuthorizationService authorizationService)
|
||||||
{
|
{
|
||||||
this.authorizationServerSettings = authorizationServerSettings;
|
this.authorizationServerSettings = authorizationServerSettings;
|
||||||
this.registeredClientRepository = registeredClientRepository;
|
this.registeredClientRepository = registeredClientRepository;
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/oauth2/consent")
|
@GetMapping("/oauth2/consent")
|
||||||
@ -61,6 +71,31 @@ public class ConsentController {
|
|||||||
return "consent";
|
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) {
|
private static String getClientDomain(final RegisteredClient client) {
|
||||||
return client.getRedirectUris()
|
return client.getRedirectUris()
|
||||||
.stream()
|
.stream()
|
||||||
@ -89,4 +124,9 @@ public class ConsentController {
|
|||||||
public Authentication authentication(Authentication authentication) {
|
public Authentication authentication(Authentication authentication) {
|
||||||
return authentication;
|
return authentication;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ModelAttribute("csrfToken")
|
||||||
|
public CsrfToken csrfToken(CsrfToken csrfToken) {
|
||||||
|
return csrfToken;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
@import org.springframework.security.core.Authentication
|
@import org.springframework.security.core.Authentication
|
||||||
|
@import org.springframework.security.web.csrf.CsrfToken
|
||||||
@import java.util.Set
|
@import java.util.Set
|
||||||
|
|
||||||
@param String clientId
|
@param String clientId
|
||||||
@ -8,6 +9,7 @@
|
|||||||
@param String state
|
@param String state
|
||||||
@param Authentication currentUser
|
@param Authentication currentUser
|
||||||
@param Set<String> scopes
|
@param Set<String> scopes
|
||||||
|
@param CsrfToken csrfToken
|
||||||
|
|
||||||
@template.base(title = "Consent", content = @`
|
@template.base(title = "Consent", content = @`
|
||||||
<h1>Consent</h1>
|
<h1>Consent</h1>
|
||||||
@ -23,6 +25,7 @@
|
|||||||
<form method="post" action="${authorizationUrl}">
|
<form method="post" action="${authorizationUrl}">
|
||||||
<input type="hidden" name="client_id" value="${clientId}">
|
<input type="hidden" name="client_id" value="${clientId}">
|
||||||
<input type="hidden" name="state" value="${state}">
|
<input type="hidden" name="state" value="${state}">
|
||||||
|
<input type="hidden" name="${csrfToken.getParameterName()}" value="${csrfToken.getToken()}">
|
||||||
|
|
||||||
<ul class="list-group mb-3">
|
<ul class="list-group mb-3">
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
|
|||||||
@ -23,7 +23,9 @@ import java.util.Set;
|
|||||||
|
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.not;
|
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.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.get;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
@ -99,6 +101,41 @@ public class ConsentFlowTest extends AbstractMetadataTest {
|
|||||||
.andExpect(content().string(not(containsString("openid"))));
|
.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 {
|
private ResultActions attemptAuthorizationWithConsentResponse(String principal) throws Exception {
|
||||||
Set<String> scopes = Set.of();
|
Set<String> scopes = Set.of();
|
||||||
return attemptAuthorizationWithConsentResponseUsingScopes(principal, scopes);
|
return attemptAuthorizationWithConsentResponseUsingScopes(principal, scopes);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user