KEYCLOAK-1295 Fixes and javadoc

This commit is contained in:
mposolda 2015-08-21 09:49:19 +02:00
parent c9afe8fcc4
commit b0e2624343
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.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() {

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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<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) {
ClientCredentialsProvider authenticator = deployment.getClientAuthenticator();
authenticator.setClientCredentials(deployment, requestHeaders, formparams);

View file

@ -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 <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>
*/

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -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

View file

@ -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 <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 {
// 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<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
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));

View file

@ -5,6 +5,7 @@ import java.util.Map;
import org.keycloak.models.ClientModel;
/**
* Encapsulates information about the execution in ClientAuthenticationFlow
*
* @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;
/**
* 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>
*/
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
*/

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ -19,7 +23,7 @@ public interface ClientAuthenticatorFactory extends ProviderFactory<ClientAuthen
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
*/

View file

@ -22,7 +22,9 @@ import org.keycloak.provider.ProviderConfigProperty;
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>
*/

View file

@ -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 <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>
*/
public class JWTClientAuthenticator extends AbstractClientAuthenticator {