From b0e2624343afed04a238e560907d66a753a80463 Mon Sep 17 00:00:00 2001 From: mposolda Date: Fri, 21 Aug 2015 09:49:19 +0200 Subject: [PATCH] KEYCLOAK-1295 Fixes and javadoc --- .../admin/resources/js/controllers/clients.js | 6 +- .../ClientCredentialsProvider.java | 44 ++++++- .../ClientCredentialsProviderUtils.java | 3 + .../JWTClientCredentialsProvider.java | 3 +- ...KeycloakDeploymentDelegateOAuthClient.java | 6 +- .../keycloak/servlet/ServletOAuthClient.java | 110 ++++++++++++++++-- .../ServletOAuthClientBuilderTest.java | 4 +- .../ClientAuthenticationFlowContext.java | 1 + .../authentication/ClientAuthenticator.java | 13 ++- .../ClientAuthenticatorFactory.java | 8 +- .../ClientIdAndSecretAuthenticator.java | 4 +- .../client/JWTClientAuthenticator.java | 6 + 12 files changed, 185 insertions(+), 23 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index c6924c9c6f..680684561b 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -74,18 +74,18 @@ module.controller('ClientSignedJWTCtrl', function($scope, $location, realm, clie $scope.realm = realm; $scope.client = client; - var signingKeyInfo = ClientCertificate.get({ realm : realm.realm, client : client.id, attribute: 'jwt.credentials' }, + var signingKeyInfo = ClientCertificate.get({ realm : realm.realm, client : client.id, attribute: 'jwt.credential' }, function() { $scope.signingKeyInfo = signingKeyInfo; } ); $scope.importCertificate = function() { - $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/import/jwt.credentials"); + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/import/jwt.credential"); }; $scope.generateSigningKey = function() { - $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/export/jwt.credentials"); + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/export/jwt.credential"); }; $scope.cancel = function() { diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java index b7e7a28e63..38b0e038ff 100644 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java @@ -5,15 +5,57 @@ import java.util.Map; import org.keycloak.adapters.KeycloakDeployment; /** - * TODO: Javadoc + * The simple SPI for authenticating clients/applications . It's used by adapter during all OIDC backchannel requests to Keycloak server + * (codeToToken exchange, refresh token or backchannel logout) . You can also use it in your application during direct access grants or service account request + * (See the service-account example from Keycloak demo for more info) + * + * When you implement this SPI on the adapter (application) side, you also need to implement {@link org.keycloak.authentication.ClientAuthenticator} on the server side, + * so your server is able to authenticate client + * + * You must specify a file + * META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider in the WAR that this class is contained in (or in the JAR that is attached to the WEB-INF/lib or as jboss module + * if you want to share the implementation among more WARs). This file must have the fully qualified class name of all your ClientAuthenticatorFactory classes + * + * NOTE: The SPI is not finished and method signatures are still subject to change in future versions (for example to support usecase for + * authentication with client certificate) + * + * @see ClientIdAndSecretCredentialsProvider + * @see JWTClientCredentialsProvider * * @author Marek Posolda */ public interface ClientCredentialsProvider { + /** + * Return the ID of the provider. Use this ID in the keycloak.json configuration as the subelement of the "credentials" element + * + * For example if your provider has ID "kerberos-keytab" , use the configuration like this in keycloak.json + * + * "credentials": { + * + * "kerberos-keytab": { + * "keytab": "/tmp/foo" + * } + * } + * + * @return + */ String getId(); + /** + * Called by adapter during deployment of your application. You can for example read configuration and init your authenticator here + * + * @param deployment the adapter configuration + * @param config the configuration of your provider read from keycloak.json . For the kerberos-keytab example above, it will return map with the single key "keytab" with value "/tmp/foo" + */ void init(KeycloakDeployment deployment, Object config); + /** + * Called every time adapter needs to perform backchannel request + * + * @param deployment Fully resolved deployment + * @param requestHeaders You should put any HTTP request headers you want to use for authentication of client. These headers will be attached to the HTTP request sent to Keycloak server + * @param formParams You should put any request parameters you want to use for authentication of client. These parameters will be attached to the HTTP request sent to Keycloak server + */ void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formParams); } diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java index df8b8809ba..be3eb41e55 100644 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java @@ -60,6 +60,9 @@ public class ClientCredentialsProviderUtils { } } + /** + * Use this method when calling backchannel request directly from your application. See service-account example from demo for more details + */ public static void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formparams) { ClientCredentialsProvider authenticator = deployment.getClientAuthenticator(); authenticator.setClientCredentials(deployment, requestHeaders, formparams); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java index e249a43231..6503e678d9 100644 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java @@ -12,7 +12,8 @@ import org.keycloak.util.KeystoreUtil; import org.keycloak.util.Time; /** - * Client authentication based on JWT signed by client private key + * Client authentication based on JWT signed by client private key . + * See specs for more details. * * @author Marek Posolda */ diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java index d24b0b4189..fc2445ba46 100644 --- a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java +++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java @@ -4,7 +4,9 @@ import java.util.Map; import org.keycloak.AbstractOAuthClient; import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.enums.RelativeUrlsUsed; +import org.keycloak.util.KeycloakUriBuilder; /** * @author Marek Posolda @@ -43,7 +45,7 @@ public class KeycloakDeploymentDelegateOAuthClient extends AbstractOAuthClient { @Override public String getAuthUrl() { - return deployment.getAuthUrl().clone().build().toString(); + throw new IllegalStateException("Illegal to call this method. Use KeycloakDeployment to resolve correct deployment for this request"); } @Override @@ -53,7 +55,7 @@ public class KeycloakDeploymentDelegateOAuthClient extends AbstractOAuthClient { @Override public String getTokenUrl() { - return deployment.getTokenUrl(); + throw new IllegalStateException("Illegal to call this method. Use KeycloakDeployment to resolve correct deployment for this request"); } @Override diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java index 3d61013300..72501acbf9 100755 --- a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java +++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java @@ -1,18 +1,26 @@ package org.keycloak.servlet; +import org.keycloak.KeycloakSecurityContext; import org.keycloak.OAuth2Constants; +import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.HttpFacade; +import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.ServerRequest; +import org.keycloak.enums.RelativeUrlsUsed; import org.keycloak.jose.jws.JWSInput; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; import org.keycloak.util.KeycloakUriBuilder; import org.keycloak.util.UriUtils; +import javax.security.cert.X509Certificate; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.InputStream; import java.net.URI; +import java.util.List; /** * @author Bill Burke @@ -29,7 +37,8 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient { private AccessTokenResponse resolveBearerToken(HttpServletRequest request, String redirectUri, String code) throws IOException, ServerRequest.HttpFailure { // Don't send sessionId in oauth clients for now - return ServerRequest.invokeAccessCodeToToken(getDeployment(), code, redirectUri, null); + KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request); + return ServerRequest.invokeAccessCodeToToken(resolvedDeployment, code, redirectUri, null); } /** @@ -62,10 +71,12 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient { */ public void redirect(String redirectUri, HttpServletRequest request, HttpServletResponse response) throws IOException { String state = getStateCode(); + KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request); + String authUrl = resolvedDeployment.getAuthUrl().clone().build().toString(); - KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(getUrl(request, authUrl, true)) + KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(authUrl) .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE) - .queryParam(OAuth2Constants.CLIENT_ID, clientId) + .queryParam(OAuth2Constants.CLIENT_ID, getClientId()) .queryParam(OAuth2Constants.REDIRECT_URI, redirectUri) .queryParam(OAuth2Constants.STATE, state); if (scope != null) { @@ -136,7 +147,8 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient { } public AccessTokenResponse refreshToken(HttpServletRequest request, String refreshToken) throws IOException, ServerRequest.HttpFailure { - return ServerRequest.invokeRefresh(getDeployment(), refreshToken); + KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request); + return ServerRequest.invokeRefresh(resolvedDeployment, refreshToken); } public static IDToken extractIdToken(String idToken) { @@ -149,12 +161,90 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient { } } - private String getUrl(HttpServletRequest request, String url, boolean isBrowserRequest) { - if (relativeUrlsUsed.useRelative(isBrowserRequest)) { - String baseUrl = UriUtils.getOrigin(request.getRequestURL().toString()); - return baseUrl + url; - } else { - return url; + private KeycloakDeployment resolveDeployment(KeycloakDeployment baseDeployment, HttpServletRequest request) { + ServletFacade facade = new ServletFacade(request); + return new AdapterDeploymentContext(baseDeployment).resolveDeployment(facade); + } + + + public static class ServletFacade implements HttpFacade { + + private final HttpServletRequest servletRequest; + + private ServletFacade(HttpServletRequest servletRequest) { + this.servletRequest = servletRequest; + } + + @Override + public KeycloakSecurityContext getSecurityContext() { + throw new IllegalStateException("Not yet implemented"); + } + + @Override + public Request getRequest() { + return new Request() { + + @Override + public String getMethod() { + return servletRequest.getMethod(); + } + + @Override + public String getURI() { + return servletRequest.getRequestURL().toString(); + } + + @Override + public boolean isSecure() { + return servletRequest.isSecure(); + } + + @Override + public String getQueryParamValue(String param) { + return servletRequest.getParameter(param); + } + + @Override + public Cookie getCookie(String cookieName) { + // TODO + return null; + } + + @Override + public String getHeader(String name) { + return servletRequest.getHeader(name); + } + + @Override + public List getHeaders(String name) { + // TODO + return null; + } + + @Override + public InputStream getInputStream() { + try { + return servletRequest.getInputStream(); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + + @Override + public String getRemoteAddr() { + return servletRequest.getRemoteAddr(); + } + }; + } + + @Override + public Response getResponse() { + throw new IllegalStateException("Not yet implemented"); + } + + @Override + public X509Certificate[] getCertificateChain() { + throw new IllegalStateException("Not yet implemented"); } } } diff --git a/integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java b/integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java index 604b58cfe8..d8f78e2744 100644 --- a/integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java +++ b/integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java @@ -15,8 +15,8 @@ public class ServletOAuthClientBuilderTest { @Test public void testBuilder() { ServletOAuthClient oauthClient = ServletOAuthClientBuilder.build(getClass().getResourceAsStream("/keycloak.json")); - Assert.assertEquals("https://localhost:8443/auth/realms/demo/protocol/openid-connect/auth", oauthClient.getAuthUrl()); - Assert.assertEquals("https://backend:8443/auth/realms/demo/protocol/openid-connect/token", oauthClient.getTokenUrl()); + Assert.assertEquals("https://localhost:8443/auth/realms/demo/protocol/openid-connect/auth", oauthClient.getDeployment().getAuthUrl().clone().build().toString()); + Assert.assertEquals("https://backend:8443/auth/realms/demo/protocol/openid-connect/token", oauthClient.getDeployment().getTokenUrl()); assertEquals(RelativeUrlsUsed.NEVER, oauthClient.getRelativeUrlsUsed()); Assert.assertEquals("customer-portal", oauthClient.getClientId()); Assert.assertEquals("234234-234234-234234", oauthClient.getCredentials().get(CredentialRepresentation.SECRET)); diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java index 9f11eb97fc..3dba809272 100644 --- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java +++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java @@ -5,6 +5,7 @@ import java.util.Map; import org.keycloak.models.ClientModel; /** + * Encapsulates information about the execution in ClientAuthenticationFlow * * @author Marek Posolda */ diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java index 4cbf2ef349..47c0d5de3e 100644 --- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java @@ -6,12 +6,23 @@ import org.keycloak.models.RealmModel; import org.keycloak.provider.Provider; /** + * This interface is for users that want to add custom client authenticators to an authentication flow. + * You must implement this interface as well as a ClientAuthenticatorFactory. + * + * This interface is for verifying client credentials from request. On the adapter side, you must also implement org.keycloak.adapters.authentication.ClientCredentialsProvider , which is supposed + * to add the client credentials to the request, which will ClientAuthenticator verify on server side + * + * @see org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator + * @see org.keycloak.authentication.authenticators.client.JWTClientAuthenticator + * * @author Marek Posolda */ public interface ClientAuthenticator extends Provider { /** - * TODO: javadoc + * Initial call for the authenticator. This method should check the current HTTP request to determine if the request + * satisfies the ClientAuthenticator's requirements. If it doesn't, it should send back a challenge response by calling + * the ClientAuthenticationFlowContext.challenge(Response). * * @param context */ diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java index 9f9cc86d81..18ddaa5517 100644 --- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java @@ -3,7 +3,11 @@ package org.keycloak.authentication; import org.keycloak.provider.ProviderFactory; /** - * TODO + * Factory for creating ClientAuthenticator instances. This is a singleton and created when Keycloak boots. + * + * You must specify a file + * META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory in the jar that this class is contained in + * This file must have the fully qualified class name of all your ClientAuthenticatorFactory classes * * @author Marek Posolda */ @@ -19,7 +23,7 @@ public interface ClientAuthenticatorFactory extends ProviderFactoryMarek Posolda */ diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java index 3f7ddb6c15..db0cb9fcec 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java @@ -24,6 +24,12 @@ import org.keycloak.representations.JsonWebToken; import org.keycloak.services.Urls; /** + * Client authentication based on JWT signed by client private key . + * See specs for more details. + * + * This is server side, which verifies JWT from client_assertion parameter, where the assertion was created on adapter side by + * org.keycloak.adapters.authentication.JWTClientCredentialsProvider + * * @author Marek Posolda */ public class JWTClientAuthenticator extends AbstractClientAuthenticator {