Testsuite PoC - Prototype OAuth client (#31663)

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen 2024-07-26 15:01:36 +02:00 committed by GitHub
parent 4d66f3886b
commit 34f4eeedd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 371 additions and 2 deletions

View file

@ -0,0 +1,90 @@
package org.keycloak.test.examples;
import com.nimbusds.oauth2.sdk.AuthorizationResponse;
import com.nimbusds.oauth2.sdk.TokenIntrospectionResponse;
import com.nimbusds.oauth2.sdk.TokenResponse;
import com.nimbusds.oauth2.sdk.token.AccessToken;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.test.framework.annotations.InjectOAuthClient;
import org.keycloak.test.framework.annotations.InjectPage;
import org.keycloak.test.framework.annotations.InjectRealm;
import org.keycloak.test.framework.annotations.InjectUser;
import org.keycloak.test.framework.annotations.InjectWebDriver;
import org.keycloak.test.framework.annotations.KeycloakIntegrationTest;
import org.keycloak.test.framework.oauth.OAuthClient;
import org.keycloak.test.framework.page.LoginPage;
import org.keycloak.test.framework.realm.ManagedRealm;
import org.keycloak.test.framework.realm.ManagedUser;
import org.openqa.selenium.WebDriver;
import java.net.URI;
import java.net.URL;
@KeycloakIntegrationTest
public class OAuthClientTest {
@InjectRealm
ManagedRealm realm; // Need to specify realm as otherwise there's a bug when annotation is not present
@InjectUser(config = UserConfig.class)
ManagedUser user;
@InjectOAuthClient
OAuthClient oAuthClient;
@InjectWebDriver
WebDriver webDriver;
@InjectPage
LoginPage loginPage;
@Test
public void testClientCredentials() throws Exception {
TokenResponse tokenResponse = oAuthClient.clientCredentialGrant();
Assertions.assertTrue(tokenResponse.indicatesSuccess());
Assertions.assertNotNull(tokenResponse.toSuccessResponse().getTokens().getAccessToken());
}
@Test
public void testIntrospection() throws Exception {
AccessToken accessToken = oAuthClient.clientCredentialGrant().toSuccessResponse().getTokens().getAccessToken();
TokenIntrospectionResponse introspectionResponse = oAuthClient.introspection(accessToken);
Assertions.assertTrue(introspectionResponse.indicatesSuccess());
Assertions.assertNotNull(introspectionResponse.toSuccessResponse().getIssuer());
}
@Test
public void testAuthorizationCode() throws Exception {
URL authorizationRequestURL = oAuthClient.authorizationRequest();
webDriver.navigate().to(authorizationRequestURL);
loginPage.fillLogin(user.getUsername(), user.getPassword());
loginPage.submit();
Assertions.assertEquals(1, oAuthClient.getCallbacks().size());
URI callbackUri = oAuthClient.getCallbacks().remove(0);
AuthorizationResponse authorizationResponse = AuthorizationResponse.parse(callbackUri);
Assertions.assertTrue(authorizationResponse.indicatesSuccess());
Assertions.assertNotNull(authorizationResponse.toSuccessResponse().getAuthorizationCode());
TokenResponse tokenResponse = oAuthClient.tokenRequest(authorizationResponse.toSuccessResponse().getAuthorizationCode());
Assertions.assertTrue(tokenResponse.indicatesSuccess());
Assertions.assertNotNull(tokenResponse.toSuccessResponse().getTokens().getAccessToken());
}
public static class UserConfig implements org.keycloak.test.framework.realm.UserConfig {
@Override
public UserRepresentation getRepresentation() {
return builder()
.name("First", "Last")
.email("test@local")
.password("password")
.build();
}
}
}

View file

@ -86,5 +86,11 @@
<groupId>org.testcontainers</groupId>
<artifactId>mssqlserver</artifactId>
</dependency>
<!-- Temporary dependency until we figure out how we want to support OAuth -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>11.13</version>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,17 @@
package org.keycloak.test.framework.annotations;
import org.keycloak.test.framework.oauth.DefaultOAuthClientConfiguration;
import org.keycloak.test.framework.realm.ClientConfig;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectOAuthClient {
Class<? extends ClientConfig> config() default DefaultOAuthClientConfiguration.class;
}

View file

@ -0,0 +1,18 @@
package org.keycloak.test.framework.oauth;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.test.framework.realm.ClientConfig;
public class DefaultOAuthClientConfiguration implements ClientConfig {
@Override
public ClientRepresentation getRepresentation() {
return builder()
.clientId("test-oauth-client")
.serviceAccount()
.redirectUris("http://127.0.0.1/callback/oauth")
.secret("test-secret")
.build();
}
}

View file

@ -0,0 +1,60 @@
package org.keycloak.test.framework.oauth;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.List;
class OAuthCallbackServer {
private final HttpServer httpServer;
private final OAuthCallbackHandler callbackHandler;
private final URI redirectionUri;
public OAuthCallbackServer() {
this.callbackHandler = new OAuthCallbackHandler();
try {
httpServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
httpServer.createContext("/callback/oauth", callbackHandler);
httpServer.start();
redirectionUri = new URI("http://127.0.0.1:" + httpServer.getAddress().getPort() + "/callback/oauth");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public URI getRedirectionUri() {
return redirectionUri;
}
public List<URI> getCallbacks() {
return callbackHandler.callbacks;
}
public void close() {
httpServer.stop(0);
}
static class OAuthCallbackHandler implements HttpHandler {
private List<URI> callbacks = new LinkedList<>();
@Override
public void handle(HttpExchange exchange) throws IOException {
callbacks.add(exchange.getRequestURI());
byte[] happydays = new String("<html><body>Happy days</body></html>").getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "text/html");
exchange.sendResponseHeaders(200, happydays.length);
exchange.getResponseBody().write(happydays);
exchange.getResponseBody().close();
}
}
}

View file

@ -0,0 +1,119 @@
package org.keycloak.test.framework.oauth;
import com.nimbusds.oauth2.sdk.AuthorizationCode;
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.AuthorizationRequest;
import com.nimbusds.oauth2.sdk.ClientCredentialsGrant;
import com.nimbusds.oauth2.sdk.GeneralException;
import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.oauth2.sdk.TokenIntrospectionRequest;
import com.nimbusds.oauth2.sdk.TokenIntrospectionResponse;
import com.nimbusds.oauth2.sdk.TokenRequest;
import com.nimbusds.oauth2.sdk.TokenResponse;
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
import com.nimbusds.oauth2.sdk.auth.Secret;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.id.Issuer;
import com.nimbusds.oauth2.sdk.id.State;
import com.nimbusds.oauth2.sdk.token.AccessToken;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import jakarta.ws.rs.core.Response;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.test.framework.realm.ApiUtil;
import org.keycloak.test.framework.realm.ClientConfig;
import org.keycloak.test.framework.realm.ManagedClient;
import org.keycloak.test.framework.realm.ManagedRealm;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.List;
public class OAuthClient {
private final ManagedRealm realm;
private final ManagedClient client;
private final OAuthCallbackServer callbackServer;
private OIDCProviderMetadata oidcProviderMetadata;
public OAuthClient(ManagedRealm realm, ClientConfig clientConfig) {
this.realm = realm;
this.client = registerClient(clientConfig);
this.callbackServer = new OAuthCallbackServer();
}
private ManagedClient registerClient(ClientConfig clientConfig) {
ClientRepresentation clientRepresentation = clientConfig.getRepresentation();
Response response = realm.admin().clients().create(clientRepresentation);
String id = ApiUtil.handleCreatedResponse(response);
clientRepresentation.setId(id);
return new ManagedClient(clientRepresentation, realm.admin().clients().get(id));
}
public TokenResponse clientCredentialGrant() throws IOException, GeneralException {
AuthorizationGrant clientGrant = new ClientCredentialsGrant();
ClientAuthentication clientAuthentication = getClientAuthentication();
URI tokenEndpoint = getOIDCProviderMetadata().getTokenEndpointURI();
TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientAuthentication, clientGrant);
return TokenResponse.parse(tokenRequest.toHTTPRequest().send());
}
public TokenResponse tokenRequest(AuthorizationCode authorizationCode) throws IOException, GeneralException {
AuthorizationGrant grant = new AuthorizationCodeGrant(authorizationCode, callbackServer.getRedirectionUri());
ClientAuthentication clientAuthentication = getClientAuthentication();
URI tokenEndpoint = getOIDCProviderMetadata().getTokenEndpointURI();
TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientAuthentication, grant);
return TokenResponse.parse(tokenRequest.toHTTPRequest().send());
}
public TokenIntrospectionResponse introspection(AccessToken accessToken) throws IOException, GeneralException {
ClientAuthentication clientAuthentication = getClientAuthentication();
URI introspectionEndpoint = getOIDCProviderMetadata().getIntrospectionEndpointURI();
TokenIntrospectionRequest introspectionRequest = new TokenIntrospectionRequest(introspectionEndpoint, clientAuthentication, accessToken);
return TokenIntrospectionResponse.parse(introspectionRequest.toHTTPRequest().send());
}
public URL authorizationRequest() throws IOException, GeneralException {
URI authorizationEndpoint = getOIDCProviderMetadata().getAuthorizationEndpointURI();
State state = new State();
ClientID clientID = new ClientID(client.getClientId());
AuthorizationRequest authorizationRequest = new AuthorizationRequest.Builder(new ResponseType(ResponseType.Value.CODE), clientID)
.state(state)
.redirectionURI(callbackServer.getRedirectionUri())
.endpointURI(authorizationEndpoint)
.build();
return authorizationRequest.toURI().toURL();
}
public List<URI> getCallbacks() {
return callbackServer.getCallbacks();
}
public void close() {
client.admin().remove();
callbackServer.close();
}
private ClientAuthentication getClientAuthentication() {
ClientID clientID = new ClientID(client.getClientId());
Secret clientSecret = new Secret(client.getSecret());
return new ClientSecretBasic(clientID, clientSecret);
}
private OIDCProviderMetadata getOIDCProviderMetadata() throws GeneralException, IOException {
if (oidcProviderMetadata == null) {
Issuer issuer = new Issuer(realm.getBaseUrl());
oidcProviderMetadata = OIDCProviderMetadata.resolve(issuer);
}
return oidcProviderMetadata;
}
}

View file

@ -0,0 +1,45 @@
package org.keycloak.test.framework.oauth;
import org.keycloak.test.framework.annotations.InjectOAuthClient;
import org.keycloak.test.framework.injection.InstanceContext;
import org.keycloak.test.framework.injection.LifeCycle;
import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
import org.keycloak.test.framework.injection.SupplierHelpers;
import org.keycloak.test.framework.realm.ClientConfig;
import org.keycloak.test.framework.realm.ManagedRealm;
public class OAuthClientSupplier implements Supplier<OAuthClient, InjectOAuthClient> {
@Override
public Class<InjectOAuthClient> getAnnotationClass() {
return InjectOAuthClient.class;
}
@Override
public Class<OAuthClient> getValueType() {
return OAuthClient.class;
}
@Override
public OAuthClient getValue(InstanceContext<OAuthClient, InjectOAuthClient> instanceContext) {
ManagedRealm realm = instanceContext.getDependency(ManagedRealm.class);
ClientConfig clientConfig = SupplierHelpers.getInstance(instanceContext.getAnnotation().config());
return new OAuthClient(realm, clientConfig);
}
@Override
public boolean compatible(InstanceContext<OAuthClient, InjectOAuthClient> a, RequestedInstance<OAuthClient, InjectOAuthClient> b) {
return true;
}
@Override
public LifeCycle getLifeCycle(InjectOAuthClient annotation) {
return LifeCycle.GLOBAL;
}
@Override
public void close(InstanceContext<OAuthClient, InjectOAuthClient> instanceContext) {
instanceContext.getValue().close();
}
}

View file

@ -16,11 +16,21 @@ public class ClientConfigBuilder {
return this;
}
public ClientConfigBuilder secret(String secret) {
representation.setSecret(secret);
return this;
}
public ClientConfigBuilder redirectUris(String... redirectUris) {
representation.setRedirectUris(Collections.combine(representation.getRedirectUris(), redirectUris));
return this;
}
public ClientConfigBuilder serviceAccount() {
representation.setServiceAccountsEnabled(true);
return this;
}
public ClientRepresentation build() {
return representation;
}

View file

@ -21,6 +21,10 @@ public class ManagedClient {
return createdRepresentation.getClientId();
}
public String getSecret() {
return createdRepresentation.getSecret();
}
public ClientResource admin() {
return clientResource;
}

View file

@ -2,7 +2,6 @@ package org.keycloak.test.framework.realm;
import jakarta.ws.rs.core.Response;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.test.framework.annotations.InjectUser;
import org.keycloak.test.framework.injection.InstanceContext;

View file

@ -15,4 +15,5 @@ org.keycloak.test.framework.database.MySQLDatabaseSupplier
org.keycloak.test.framework.database.PostgresDatabaseSupplier
org.keycloak.test.framework.database.MariaDBDatabaseSupplier
org.keycloak.test.framework.database.MSSQLServerDatabaseSupplier
org.keycloak.test.framework.page.PageSupplier
org.keycloak.test.framework.page.PageSupplier
org.keycloak.test.framework.oauth.OAuthClientSupplier