From e9a82c0135e9b04ea3716829a246f18ff5cea3a3 Mon Sep 17 00:00:00 2001 From: Andreas Svanberg Date: Thu, 8 May 2025 15:16:10 +0200 Subject: [PATCH] Allow clients to authenticate using form post Nextcloud OAuth 2 login sends credentials as form parameters instead of using HTTP Basic. --- .../se/su/dsv/oauth2/admin/ClientManager.java | 8 +- .../dsv/oauth2/ClientAuthenticationTest.java | 102 ++++++++++++++++++ 2 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 src/test/java/se/su/dsv/oauth2/ClientAuthenticationTest.java diff --git a/src/main/java/se/su/dsv/oauth2/admin/ClientManager.java b/src/main/java/se/su/dsv/oauth2/admin/ClientManager.java index 1eb8ff4..966141d 100644 --- a/src/main/java/se/su/dsv/oauth2/admin/ClientManager.java +++ b/src/main/java/se/su/dsv/oauth2/admin/ClientManager.java @@ -170,9 +170,9 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme } private static RegisteredClient toRegisteredClient(final ClientRow clientRow) { - ClientAuthenticationMethod clientAuthenticationMethod = clientRow.isPublic() - ? ClientAuthenticationMethod.NONE - : ClientAuthenticationMethod.CLIENT_SECRET_BASIC; + Set clientAuthenticationMethods = clientRow.isPublic() + ? Set.of(ClientAuthenticationMethod.NONE) + : Set.of(ClientAuthenticationMethod.CLIENT_SECRET_BASIC, ClientAuthenticationMethod.CLIENT_SECRET_POST); return RegisteredClient.withId(clientRow.id()) .clientId(clientRow.clientId()) .clientSecret(clientRow.clientSecret()) @@ -191,7 +191,7 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme grantTypes.add(AuthorizationGrantType.REFRESH_TOKEN); } }) - .clientAuthenticationMethod(clientAuthenticationMethod) + .clientAuthenticationMethods(methods -> methods.addAll(clientAuthenticationMethods)) .tokenSettings(TokenSettings.builder() .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) .accessTokenTimeToLive(Duration.ofHours(1)) diff --git a/src/test/java/se/su/dsv/oauth2/ClientAuthenticationTest.java b/src/test/java/se/su/dsv/oauth2/ClientAuthenticationTest.java new file mode 100644 index 0000000..03a90ea --- /dev/null +++ b/src/test/java/se/su/dsv/oauth2/ClientAuthenticationTest.java @@ -0,0 +1,102 @@ +package se.su.dsv.oauth2; + +import com.nimbusds.jwt.JWTClaimsSet; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.transaction.annotation.Transactional; +import se.su.dsv.oauth2.admin.ClientData; +import se.su.dsv.oauth2.admin.ClientManager; +import se.su.dsv.oauth2.admin.NewClient; + +import java.net.URI; +import java.util.Set; + +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.status; +import static se.su.dsv.oauth2.ShibbolethRequestProcessor.remoteUser; + +@SpringBootTest +@Transactional +@Rollback +public class ClientAuthenticationTest extends AbstractMetadataCodeFlowTest { + public static final String REDIRECT_URI = "http://localhost"; + @Autowired + ClientManager clientManager; + + private NewClient client; + + @BeforeEach + public void registerClient() { + ClientData clientData = new ClientData("test-client", URI.create(REDIRECT_URI), false, + Set.of(), "test@localhost", false); + client = clientManager.createClient(() -> "test@local", clientData); + } + + + @Test + public void authenticate_client_using_http_basic() throws Exception { + String principal = "user"; + + MvcResult authorizeResult = mockMvc.perform(get(getAuthorizationEndpoint()) + .with(remoteUser(principal)) + .queryParam("response_type", "code") + .queryParam("client_id", client.clientId()) + .queryParam("redirect_uri", REDIRECT_URI)) + .andExpect(status().is3xxRedirection()) + .andReturn(); + + String code = extractCode(authorizeResult.getResponse().getRedirectedUrl()); + + MvcResult tokenResult = mockMvc.perform(post(getTokenEndpoint()) + .with(httpBasic(client.clientId(), client.clientSecret())) + .param("grant_type", "authorization_code") + .param("code", code) + .param("redirect_uri", REDIRECT_URI)) + .andExpect(status().isOk()) + .andReturn(); + + TokenResponse tokenResponse = getTokenResponse(tokenResult); + assertNotNull(tokenResponse.accessToken()); + + JWTClaimsSet claims = verifyToken(tokenResponse.accessToken()); + assertEquals(principal, claims.getSubject()); + } + + @Test + public void authenticate_client_using_form_post() throws Exception { + String principal = "user"; + + MvcResult authorizeResult = mockMvc.perform(get(getAuthorizationEndpoint()) + .with(remoteUser(principal)) + .queryParam("response_type", "code") + .queryParam("client_id", client.clientId()) + .queryParam("redirect_uri", REDIRECT_URI)) + .andExpect(status().is3xxRedirection()) + .andReturn(); + + String code = extractCode(authorizeResult.getResponse().getRedirectedUrl()); + + MvcResult tokenResult = mockMvc.perform(post(getTokenEndpoint()) + .param("grant_type", "authorization_code") + .param("code", code) + .param("client_id", client.clientId()) + .param("client_secret", client.clientSecret()) + .param("redirect_uri", REDIRECT_URI)) + .andExpect(status().isOk()) + .andReturn(); + + TokenResponse tokenResponse = getTokenResponse(tokenResult); + assertNotNull(tokenResponse.accessToken()); + + JWTClaimsSet claims = verifyToken(tokenResponse.accessToken()); + assertEquals(principal, claims.getSubject()); + } +} -- 2.39.5