Merge pull request #1555 from mposolda/master

KEYCLOAK-1295 Fixes and javadoc
This commit is contained in:
Marek Posolda 2015-08-21 20:41:21 +02:00
commit 76209dd899
12 changed files with 185 additions and 23 deletions

View file

@ -74,18 +74,18 @@ module.controller('ClientSignedJWTCtrl', function($scope, $location, realm, clie
$scope.realm = realm; $scope.realm = realm;
$scope.client = client; $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() { function() {
$scope.signingKeyInfo = signingKeyInfo; $scope.signingKeyInfo = signingKeyInfo;
} }
); );
$scope.importCertificate = function() { $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() { $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() { $scope.cancel = function() {

View file

@ -5,15 +5,57 @@ import java.util.Map;
import org.keycloak.adapters.KeycloakDeployment; 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public interface ClientCredentialsProvider { 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(); 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); 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<String, String> requestHeaders, Map<String, String> formParams); void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams);
} }

View file

@ -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<String, String> requestHeaders, Map<String, String> formparams) { public static void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formparams) {
ClientCredentialsProvider authenticator = deployment.getClientAuthenticator(); ClientCredentialsProvider authenticator = deployment.getClientAuthenticator();
authenticator.setClientCredentials(deployment, requestHeaders, formparams); authenticator.setClientCredentials(deployment, requestHeaders, formparams);

View file

@ -12,7 +12,8 @@ import org.keycloak.util.KeystoreUtil;
import org.keycloak.util.Time; 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 <a href="https://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03">specs</a> for more details.
* *
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */

View file

@ -4,7 +4,9 @@ import java.util.Map;
import org.keycloak.AbstractOAuthClient; import org.keycloak.AbstractOAuthClient;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.enums.RelativeUrlsUsed; import org.keycloak.enums.RelativeUrlsUsed;
import org.keycloak.util.KeycloakUriBuilder;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -43,7 +45,7 @@ public class KeycloakDeploymentDelegateOAuthClient extends AbstractOAuthClient {
@Override @Override
public String getAuthUrl() { 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 @Override
@ -53,7 +55,7 @@ public class KeycloakDeploymentDelegateOAuthClient extends AbstractOAuthClient {
@Override @Override
public String getTokenUrl() { public String getTokenUrl() {
return deployment.getTokenUrl(); throw new IllegalStateException("Illegal to call this method. Use KeycloakDeployment to resolve correct deployment for this request");
} }
@Override @Override

View file

@ -1,18 +1,26 @@
package org.keycloak.servlet; package org.keycloak.servlet;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.OAuth2Constants; 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.adapters.ServerRequest;
import org.keycloak.enums.RelativeUrlsUsed;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import org.keycloak.util.KeycloakUriBuilder; import org.keycloak.util.KeycloakUriBuilder;
import org.keycloak.util.UriUtils; import org.keycloak.util.UriUtils;
import javax.security.cert.X509Certificate;
import javax.servlet.http.Cookie; import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.util.List;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -29,7 +37,8 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
private AccessTokenResponse resolveBearerToken(HttpServletRequest request, String redirectUri, String code) throws IOException, ServerRequest.HttpFailure { private AccessTokenResponse resolveBearerToken(HttpServletRequest request, String redirectUri, String code) throws IOException, ServerRequest.HttpFailure {
// Don't send sessionId in oauth clients for now // 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 { public void redirect(String redirectUri, HttpServletRequest request, HttpServletResponse response) throws IOException {
String state = getStateCode(); 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.RESPONSE_TYPE, OAuth2Constants.CODE)
.queryParam(OAuth2Constants.CLIENT_ID, clientId) .queryParam(OAuth2Constants.CLIENT_ID, getClientId())
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri) .queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
.queryParam(OAuth2Constants.STATE, state); .queryParam(OAuth2Constants.STATE, state);
if (scope != null) { if (scope != null) {
@ -136,7 +147,8 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
} }
public AccessTokenResponse refreshToken(HttpServletRequest request, String refreshToken) throws IOException, ServerRequest.HttpFailure { 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) { public static IDToken extractIdToken(String idToken) {
@ -149,12 +161,90 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
} }
} }
private String getUrl(HttpServletRequest request, String url, boolean isBrowserRequest) { private KeycloakDeployment resolveDeployment(KeycloakDeployment baseDeployment, HttpServletRequest request) {
if (relativeUrlsUsed.useRelative(isBrowserRequest)) { ServletFacade facade = new ServletFacade(request);
String baseUrl = UriUtils.getOrigin(request.getRequestURL().toString()); return new AdapterDeploymentContext(baseDeployment).resolveDeployment(facade);
return baseUrl + url; }
} else {
return url;
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<String> 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");
} }
} }
} }

View file

@ -15,8 +15,8 @@ public class ServletOAuthClientBuilderTest {
@Test @Test
public void testBuilder() { public void testBuilder() {
ServletOAuthClient oauthClient = ServletOAuthClientBuilder.build(getClass().getResourceAsStream("/keycloak.json")); 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://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.getTokenUrl()); Assert.assertEquals("https://backend:8443/auth/realms/demo/protocol/openid-connect/token", oauthClient.getDeployment().getTokenUrl());
assertEquals(RelativeUrlsUsed.NEVER, oauthClient.getRelativeUrlsUsed()); assertEquals(RelativeUrlsUsed.NEVER, oauthClient.getRelativeUrlsUsed());
Assert.assertEquals("customer-portal", oauthClient.getClientId()); Assert.assertEquals("customer-portal", oauthClient.getClientId());
Assert.assertEquals("234234-234234-234234", oauthClient.getCredentials().get(CredentialRepresentation.SECRET)); Assert.assertEquals("234234-234234-234234", oauthClient.getCredentials().get(CredentialRepresentation.SECRET));

View file

@ -5,6 +5,7 @@ import java.util.Map;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
/** /**
* Encapsulates information about the execution in ClientAuthenticationFlow
* *
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */

View file

@ -6,12 +6,23 @@ import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider; 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public interface ClientAuthenticator extends Provider { 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 * @param context
*/ */

View file

@ -3,7 +3,11 @@ package org.keycloak.authentication;
import org.keycloak.provider.ProviderFactory; 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
@ -19,7 +23,7 @@ public interface ClientAuthenticatorFactory extends ProviderFactory<ClientAuthen
boolean isConfigurable(); boolean isConfigurable();
/** /**
* Is this authenticator configurable per client? * Is this authenticator configurable per client? The configuration will be in "Clients" / "Credentials" tab in admin console
* *
* @return * @return
*/ */

View file

@ -22,7 +22,9 @@ import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.BasicAuthHelper;
/** /**
* Validates client based on "client_id" and "client_secret" sent either in request parameters or in "Authorization: Basic" header * Validates client based on "client_id" and "client_secret" sent either in request parameters or in "Authorization: Basic" header .
*
* See org.keycloak.adapters.authentication.ClientIdAndSecretAuthenticator for the adapter
* *
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */

View file

@ -24,6 +24,12 @@ import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
/** /**
* Client authentication based on JWT signed by client private key .
* See <a href="https://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03">specs</a> 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class JWTClientAuthenticator extends AbstractClientAuthenticator { public class JWTClientAuthenticator extends AbstractClientAuthenticator {