Testsuite PoC - Prototype OAuth client (#31663)
Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
parent
4d66f3886b
commit
34f4eeedd8
11 changed files with 371 additions and 2 deletions
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,10 @@ public class ManagedClient {
|
|||
return createdRepresentation.getClientId();
|
||||
}
|
||||
|
||||
public String getSecret() {
|
||||
return createdRepresentation.getSecret();
|
||||
}
|
||||
|
||||
public ClientResource admin() {
|
||||
return clientResource;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue