Merge pull request #1553 from mposolda/master

KEYCLOAK-1295 Adapter support. Fixes
This commit is contained in:
Marek Posolda 2015-08-21 09:20:49 +02:00
commit c9afe8fcc4
68 changed files with 954 additions and 669 deletions

View file

@ -27,26 +27,23 @@ import java.util.Map;
*/ */
public class DefaultMongoConnectionFactoryProvider implements MongoConnectionProviderFactory { public class DefaultMongoConnectionFactoryProvider implements MongoConnectionProviderFactory {
// TODO Make configurable // TODO Make it dynamic
private String[] entities = new String[]{ private String[] entities = new String[]{
"org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity", "org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoUserEntity", "org.keycloak.models.mongo.keycloak.entities.MongoUserEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity", "org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoClientEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity",
"org.keycloak.models.entities.IdentityProviderEntity", "org.keycloak.models.entities.IdentityProviderEntity",
"org.keycloak.models.entities.ClientIdentityProviderMappingEntity", "org.keycloak.models.entities.ClientIdentityProviderMappingEntity",
"org.keycloak.models.entities.RequiredCredentialEntity", "org.keycloak.models.entities.RequiredCredentialEntity",
"org.keycloak.models.entities.CredentialEntity", "org.keycloak.models.entities.CredentialEntity",
"org.keycloak.models.entities.FederatedIdentityEntity", "org.keycloak.models.entities.FederatedIdentityEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoClientEntity",
"org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity",
"org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity",
"org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity",
"org.keycloak.models.entities.UserFederationProviderEntity", "org.keycloak.models.entities.UserFederationProviderEntity",
"org.keycloak.models.entities.UserFederationMapperEntity", "org.keycloak.models.entities.UserFederationMapperEntity",
"org.keycloak.models.entities.ProtocolMapperEntity", "org.keycloak.models.entities.ProtocolMapperEntity",
"org.keycloak.models.entities.IdentityProviderMapperEntity", "org.keycloak.models.entities.IdentityProviderMapperEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity",
"org.keycloak.models.entities.AuthenticationExecutionEntity", "org.keycloak.models.entities.AuthenticationExecutionEntity",
"org.keycloak.models.entities.AuthenticationFlowEntity", "org.keycloak.models.entities.AuthenticationFlowEntity",
"org.keycloak.models.entities.AuthenticatorConfigEntity", "org.keycloak.models.entities.AuthenticatorConfigEntity",

View file

@ -16,7 +16,7 @@ public class AbstractOAuthClient {
private final AtomicLong counter = new AtomicLong(); private final AtomicLong counter = new AtomicLong();
protected String clientId; protected String clientId;
protected Map<String, String> credentials; protected Map<String, Object> credentials;
protected String authUrl; protected String authUrl;
protected String tokenUrl; protected String tokenUrl;
protected RelativeUrlsUsed relativeUrlsUsed; protected RelativeUrlsUsed relativeUrlsUsed;
@ -37,11 +37,11 @@ public class AbstractOAuthClient {
this.clientId = clientId; this.clientId = clientId;
} }
public Map<String, String> getCredentials() { public Map<String, Object> getCredentials() {
return credentials; return credentials;
} }
public void setCredentials(Map<String, String> credentials) { public void setCredentials(Map<String, Object> credentials) {
this.credentials = credentials; this.credentials = credentials;
} }

View file

@ -39,7 +39,7 @@ public class BaseAdapterConfig extends BaseRealmConfig {
@JsonProperty("public-client") @JsonProperty("public-client")
protected boolean publicClient; protected boolean publicClient;
@JsonProperty("credentials") @JsonProperty("credentials")
protected Map<String, String> credentials = new HashMap<String, String>(); protected Map<String, Object> credentials = new HashMap<>();
public boolean isUseResourceRoleMappings() { public boolean isUseResourceRoleMappings() {
@ -114,11 +114,11 @@ public class BaseAdapterConfig extends BaseRealmConfig {
this.enableBasicAuth = enableBasicAuth; this.enableBasicAuth = enableBasicAuth;
} }
public Map<String, String> getCredentials() { public Map<String, Object> getCredentials() {
return credentials; return credentials;
} }
public void setCredentials(Map<String, String> credentials) { public void setCredentials(Map<String, Object> credentials) {
this.credentials = credentials; this.credentials = credentials;
} }

View file

@ -0,0 +1,19 @@
package org.keycloak.example;
/**
* Client authentication with traditional OAuth2 client_id + client_secret
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ProductSAClientSecretServlet extends ProductServiceAccountServlet {
@Override
protected String getAdapterConfigLocation() {
return "WEB-INF/keycloak-client-secret.json";
}
@Override
protected String getClientAuthenticationMethod() {
return "secret";
}
}

View file

@ -0,0 +1,17 @@
package org.keycloak.example;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ProductSAClientSignedJWTServlet extends ProductServiceAccountServlet {
@Override
protected String getAdapterConfigLocation() {
return "WEB-INF/keycloak-client-signed-jwt.json";
}
@Override
protected String getClientAuthenticationMethod() {
return "jwt";
}
}

View file

@ -4,7 +4,9 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
@ -26,30 +28,46 @@ import org.keycloak.VerificationException;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.ServerRequest; import org.keycloak.adapters.ServerRequest;
import org.keycloak.constants.ServiceAccountConstants; import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class ProductServiceAccountServlet extends HttpServlet { public abstract class ProductServiceAccountServlet extends HttpServlet {
public static final String ERROR = "error"; public static final String ERROR = "error";
public static final String TOKEN = "token"; public static final String TOKEN = "token";
public static final String TOKEN_PARSED = "idTokenParsed"; public static final String TOKEN_PARSED = "idTokenParsed";
public static final String REFRESH_TOKEN = "refreshToken"; public static final String REFRESH_TOKEN = "refreshToken";
public static final String PRODUCTS = "products"; public static final String PRODUCTS = "products";
public static final String CLIENT_AUTH_METHOD = "clientAuthMethod";
protected abstract String getAdapterConfigLocation();
protected abstract String getClientAuthenticationMethod();
public static String getLoginUrl(HttpServletRequest request) {
return "/service-account-portal/app-" + request.getAttribute(CLIENT_AUTH_METHOD) + "/login";
}
public static String getRefreshUrl(HttpServletRequest request) {
return "/service-account-portal/app-" + request.getAttribute(CLIENT_AUTH_METHOD) + "/refresh";
}
public static String getLogoutUrl(HttpServletRequest request) {
return "/service-account-portal/app-" + request.getAttribute(CLIENT_AUTH_METHOD) + "/logout";
}
@Override @Override
public void init() throws ServletException { public void init() throws ServletException {
InputStream config = getServletContext().getResourceAsStream("WEB-INF/keycloak.json"); String adapterConfigLocation = getAdapterConfigLocation();
InputStream config = getServletContext().getResourceAsStream(adapterConfigLocation);
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config); KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
HttpClient client = new DefaultHttpClient(); getServletContext().setAttribute("deployment-" + getClientAuthenticationMethod(), deployment);
getServletContext().setAttribute(KeycloakDeployment.class.getName(), deployment); HttpClient client = new DefaultHttpClient();
getServletContext().setAttribute(HttpClient.class.getName(), client); getServletContext().setAttribute(HttpClient.class.getName(), client);
} }
@ -60,6 +78,8 @@ public class ProductServiceAccountServlet extends HttpServlet {
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setAttribute(CLIENT_AUTH_METHOD, getClientAuthenticationMethod());
String reqUri = req.getRequestURI(); String reqUri = req.getRequestURI();
if (reqUri.endsWith("/login")) { if (reqUri.endsWith("/login")) {
serviceAccountLogin(req); serviceAccountLogin(req);
@ -81,16 +101,21 @@ public class ProductServiceAccountServlet extends HttpServlet {
KeycloakDeployment deployment = getKeycloakDeployment(); KeycloakDeployment deployment = getKeycloakDeployment();
HttpClient client = getHttpClient(); HttpClient client = getHttpClient();
String clientId = deployment.getResourceName();
String clientSecret = deployment.getResourceCredentials().get("secret");
try { try {
HttpPost post = new HttpPost(deployment.getTokenUrl()); HttpPost post = new HttpPost(deployment.getTokenUrl());
List<NameValuePair> formparams = new ArrayList<NameValuePair>(); List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
String authHeader = BasicAuthHelper.createHeader(clientId, clientSecret); // Add client credentials according to the method configured in keycloak.json file
post.addHeader("Authorization", authHeader); Map<String, String> reqHeaders = new HashMap<>();
Map<String, String> reqParams = new HashMap<>();
ClientCredentialsProviderUtils.setClientCredentials(deployment, reqHeaders, reqParams);
for (Map.Entry<String, String> header : reqHeaders.entrySet()) {
post.setHeader(header.getKey(), header.getValue());
}
for (Map.Entry<String, String> param : reqParams.entrySet()) {
formparams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form); post.setEntity(form);
@ -217,7 +242,7 @@ public class ProductServiceAccountServlet extends HttpServlet {
} }
private KeycloakDeployment getKeycloakDeployment() { private KeycloakDeployment getKeycloakDeployment() {
return (KeycloakDeployment) getServletContext().getAttribute(KeycloakDeployment.class.getName()); return (KeycloakDeployment) getServletContext().getAttribute("deployment-" + getClientAuthenticationMethod());
} }
private HttpClient getHttpClient() { private HttpClient getHttpClient() {

View file

@ -0,0 +1,17 @@
{
"realm" : "demo",
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url" : "http://localhost:8080/auth",
"ssl-required" : "external",
"resource" : "product-sa-client",
"credentials": {
"jwt": {
"client-keystore-file": "classpath:keystore-client.jks",
"client-keystore-type": "JKS",
"client-keystore-password": "storepass",
"client-key-password": "keypass",
"client-key-alias": "clientkey",
"token-expiration": 10
}
}
}

View file

@ -13,10 +13,16 @@
AccessToken token = (AccessToken) request.getSession().getAttribute(ProductServiceAccountServlet.TOKEN_PARSED); AccessToken token = (AccessToken) request.getSession().getAttribute(ProductServiceAccountServlet.TOKEN_PARSED);
String products = (String) request.getAttribute(ProductServiceAccountServlet.PRODUCTS); String products = (String) request.getAttribute(ProductServiceAccountServlet.PRODUCTS);
String appError = (String) request.getAttribute(ProductServiceAccountServlet.ERROR); String appError = (String) request.getAttribute(ProductServiceAccountServlet.ERROR);
String clientAuthMethod = (String) request.getAttribute(ProductServiceAccountServlet.CLIENT_AUTH_METHOD);
String loginUrl = ProductServiceAccountServlet.getLoginUrl(request);
String refreshUrl = ProductServiceAccountServlet.getRefreshUrl(request);
String logoutUrl = ProductServiceAccountServlet.getLogoutUrl(request);
%> %>
<h1>Service account portal</h1> <h1>Service account portal</h1>
<p><a href="/service-account-portal/app/login">Login</a> | <a href="/service-account-portal/app/refresh">Refresh token</a> | <a <h2>Client authentication method: <%= clientAuthMethod %></h2>
href="/service-account-portal/app/logout">Logout</a></p> <p><a href="<%= loginUrl %>">Login</a> | <a href="<%= refreshUrl %>">Refresh token</a> | <a
href="<%= logoutUrl %>">Logout</a></p>
<hr /> <hr />
<% if (appError != null) { %> <% if (appError != null) { %>

View file

@ -7,13 +7,23 @@
<module-name>service-account-portal</module-name> <module-name>service-account-portal</module-name>
<servlet> <servlet>
<servlet-name>ServiceAccountExample</servlet-name> <servlet-name>ProductSAClientSecretServlet</servlet-name>
<servlet-class>org.keycloak.example.ProductServiceAccountServlet</servlet-class> <servlet-class>org.keycloak.example.ProductSAClientSecretServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>ProductSAClientSignedJWTServlet</servlet-name>
<servlet-class>org.keycloak.example.ProductSAClientSignedJWTServlet</servlet-class>
</servlet> </servlet>
<servlet-mapping> <servlet-mapping>
<servlet-name>ServiceAccountExample</servlet-name> <servlet-name>ProductSAClientSecretServlet</servlet-name>
<url-pattern>/app/*</url-pattern> <url-pattern>/app-secret/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>ProductSAClientSignedJWTServlet</servlet-name>
<url-pattern>/app-jwt/*</url-pattern>
</servlet-mapping> </servlet-mapping>
</web-app> </web-app>

View file

@ -1,5 +1,9 @@
<html> <html>
<head> <head><title>Service account example</title></head>
<meta http-equiv="Refresh" content="0; URL=app"> <body>
</head> <ul>
<li><a href="app-secret">Client authentication with client secret</a><br /><br /></li>
<li><a href="app-jwt">Client authentication with JWT signed by client private key</a></li>
</ul>
</body>
</html> </html>

View file

@ -173,7 +173,10 @@
"clientId": "product-sa-client", "clientId": "product-sa-client",
"enabled": true, "enabled": true,
"secret": "password", "secret": "password",
"serviceAccountsEnabled": true "serviceAccountsEnabled": true,
"attributes": {
"jwt.credential.certificate": "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ=="
}
} }
], ],
"clientScopeMappings": { "clientScopeMappings": {

View file

@ -41,7 +41,6 @@ public class AppContextListener implements ServletContextListener {
} }
ServletOAuthClientBuilder.build(is, oauthClient); ServletOAuthClientBuilder.build(is, oauthClient);
logger.info("OAuth client configured and started"); logger.info("OAuth client configured and started");
oauthClient.start();
} }
@Override @Override

View file

@ -27,8 +27,6 @@ public class Bootstrap implements ServletContextListener {
ServletContext context = sce.getServletContext(); ServletContext context = sce.getServletContext();
configureClient(context); configureClient(context);
client.start();
context.setAttribute(ServletOAuthClient.class.getName(), client); context.setAttribute(ServletOAuthClient.class.getName(), client);
} }

View file

@ -52,6 +52,11 @@
<artifactId>jboss-logging</artifactId> <artifactId>jboss-logging</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View file

@ -1,5 +1,6 @@
package org.keycloak.federation.ldap.idm.model; package org.keycloak.federation.ldap.idm.model;
import java.util.Collection;
import java.util.Deque; import java.util.Deque;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -26,6 +27,10 @@ public class LDAPDn {
@Override @Override
public String toString() { public String toString() {
return toString(entries);
}
private static String toString(Collection<Entry> entries) {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
boolean first = true; boolean first = true;
@ -62,7 +67,9 @@ public class LDAPDn {
* @return string like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org" * @return string like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org"
*/ */
public String getParentDn() { public String getParentDn() {
return new LinkedList<>(entries).remove().toString(); LinkedList<Entry> parentDnEntries = new LinkedList<>(entries);
parentDnEntries.remove();
return toString(parentDnEntries);
} }
public void addFirst(String rdnName, String rdnValue) { public void addFirst(String rdnName, String rdnValue) {

View file

@ -0,0 +1,22 @@
package org.keycloak.federation.ldap.idm.model;
import org.junit.Assert;
import org.junit.Test;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPDnTest {
@Test
public void testDn() throws Exception {
LDAPDn dn = LDAPDn.fromString("dc=keycloak, dc=org");
dn.addFirst("ou", "People");
Assert.assertEquals("ou=People,dc=keycloak,dc=org", dn.toString());
dn.addFirst("uid", "Johny,Depp");
Assert.assertEquals("uid=Johny\\,Depp,ou=People,dc=keycloak,dc=org", dn.toString());
Assert.assertEquals("ou=People,dc=keycloak,dc=org", dn.getParentDn());
}
}

View file

@ -654,7 +654,7 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
controller : 'ClientSecretCtrl' controller : 'ClientSecretCtrl'
}) })
.when('/realms/:realm/clients/:client/credentials/client-signed-jwt', { .when('/realms/:realm/clients/:client/credentials/client-jwt', {
templateUrl : resourceUrl + '/partials/client-credentials-jwt.html', templateUrl : resourceUrl + '/partials/client-credentials-jwt.html',
resolve : { resolve : {
realm : function(RealmLoader) { realm : function(RealmLoader) {
@ -666,7 +666,7 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
controller : 'ClientSignedJWTCtrl' controller : 'ClientSignedJWTCtrl'
}) })
.when('/realms/:realm/clients/:client/credentials/client-signed-jwt/:keyType/import/:attribute', { .when('/realms/:realm/clients/:client/credentials/client-jwt/:keyType/import/:attribute', {
templateUrl : resourceUrl + '/partials/client-credentials-jwt-key-import.html', templateUrl : resourceUrl + '/partials/client-credentials-jwt-key-import.html',
resolve : { resolve : {
realm : function(RealmLoader) { realm : function(RealmLoader) {
@ -681,7 +681,7 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
controller : 'ClientCertificateImportCtrl' controller : 'ClientCertificateImportCtrl'
}) })
.when('/realms/:realm/clients/:client/credentials/client-signed-jwt/:keyType/export/:attribute', { .when('/realms/:realm/clients/:client/credentials/client-jwt/:keyType/export/:attribute', {
templateUrl : resourceUrl + '/partials/client-credentials-jwt-key-export.html', templateUrl : resourceUrl + '/partials/client-credentials-jwt-key-export.html',
resolve : { resolve : {
realm : function(RealmLoader) { realm : function(RealmLoader) {

View file

@ -81,11 +81,11 @@ module.controller('ClientSignedJWTCtrl', function($scope, $location, realm, clie
); );
$scope.importCertificate = function() { $scope.importCertificate = function() {
$location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt/Signing/import/jwt.credentials"); $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/import/jwt.credentials");
}; };
$scope.generateSigningKey = function() { $scope.generateSigningKey = function() {
$location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt/Signing/export/jwt.credentials"); $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/export/jwt.credentials");
}; };
$scope.cancel = function() { $scope.cancel = function() {
@ -263,7 +263,7 @@ module.controller('ClientCertificateImportCtrl', function($scope, $location, $ht
var redirectLocation = "/realms/" + realm.realm + "/clients/" + client.id + "/saml/keys"; var redirectLocation = "/realms/" + realm.realm + "/clients/" + client.id + "/saml/keys";
} else if (callingContext == 'jwt-credentials') { } else if (callingContext == 'jwt-credentials') {
var uploadUrl = authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/upload-certificate'; var uploadUrl = authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/upload-certificate';
var redirectLocation = "/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt"; var redirectLocation = "/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt";
} }
$scope.files = []; $scope.files = [];
@ -371,6 +371,12 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht
}); });
var ext = ".jks"; var ext = ".jks";
if ($scope.jks.format == 'PKCS12') ext = ".p12"; if ($scope.jks.format == 'PKCS12') ext = ".p12";
if (callingContext == 'jwt-credentials') {
$location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt");
Notifications.success("New keypair and certificate generated successfully. Download keystore file")
}
saveAs(blob, 'keystore' + ext); saveAs(blob, 'keystore' + ext);
}).error(function(data) { }).error(function(data) {
var errorMsg = 'Error downloading'; var errorMsg = 'Error downloading';
@ -390,7 +396,7 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht
}); });
$scope.cancel = function() { $scope.cancel = function() {
$location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt"); $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt");
} }
}); });

View file

@ -1622,7 +1622,15 @@ module.controller('IdentityProviderMapperCreateCtrl', function($scope, realm, id
}); });
module.controller('RealmFlowBindingCtrl', function($scope, flows, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) { module.controller('RealmFlowBindingCtrl', function($scope, flows, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
$scope.flows = flows; $scope.flows = [];
$scope.clientFlows = [];
for (var i=0 ; i<flows.length ; i++) {
if (flows[i].providerId == 'client-flow') {
$scope.clientFlows.push(flows[i]);
} else {
$scope.flows.push(flows[i]);
}
}
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/flow-bindings"); genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/flow-bindings");
}); });
@ -1792,7 +1800,7 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo
}; };
$scope.addFlow = function() { $scope.addFlow = function() {
$location.url("/realms/" + realm.realm + '/authentication/flows/' + $scope.flow.id + '/create/flow/execution'); $location.url("/realms/" + realm.realm + '/authentication/flows/' + $scope.flow.id + '/create/flow/execution/' + $scope.flow.id);
} }
@ -1807,7 +1815,7 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo
} }
$scope.addExecution = function() { $scope.addExecution = function() {
$location.url("/realms/" + realm.realm + '/authentication/flows/' + $scope.flow.id + '/create/execution'); $location.url("/realms/" + realm.realm + '/authentication/flows/' + $scope.flow.id + '/create/execution/' + $scope.flow.id);
} }

View file

@ -46,6 +46,17 @@
<kc-tooltip>Select the flow you want to use when the user has forgotten their credentials.</kc-tooltip> <kc-tooltip>Select the flow you want to use when the user has forgotten their credentials.</kc-tooltip>
</div> </div>
<div class="form-group">
<label for="resetCredentials" class="col-md-2 control-label">Client Authentication</label>
<div class="col-md-2">
<div>
<select id="clientAuthentication" ng-model="realm.clientAuthenticationFlow" class="form-control" ng-options="flow.alias as flow.alias for flow in clientFlows">
</select>
</div>
</div>
<kc-tooltip>Select the flow you want to use for authentication of clients.</kc-tooltip>
</div>
<div class="form-group" data-ng-show="access.manageRealm"> <div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-12"> <div class="col-md-12">
<button kc-save data-ng-disabled="!changed">Save</button> <button kc-save data-ng-disabled="!changed">Save</button>

View file

@ -3,7 +3,7 @@
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li> <li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li>
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li> <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials/client-signed-jwt">Signed JWT config</a></li> <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials/client-jwt">Signed JWT config</a></li>
<li class="active">Generate Client Private Key</li> <li class="active">Generate Client Private Key</li>
</ol> </ol>

View file

@ -3,7 +3,7 @@
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li> <li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li>
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li> <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials/client-signed-jwt">Signed JWT config</a></li> <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials/client-jwt">Signed JWT config</a></li>
<li class="active">Client Certificate Import</li> <li class="active">Client Certificate Import</li>
</ol> </ol>

View file

@ -9,7 +9,7 @@
<form class="form-horizontal" name="keyForm" novalidate kc-read-only="!access.manageClients"> <form class="form-horizontal" name="keyForm" novalidate kc-read-only="!access.manageClients">
<fieldset class="form-group col-sm-10"> <fieldset class="form-group col-sm-10">
<legend uncollapsed><span class="text">Client Certificate</span> <kc-tooltip>Client Certificate for validate JWT issued by client and signed by Client private key.</kc-tooltip></legend> <legend uncollapsed><span class="text">Client Certificate</span> <kc-tooltip>Client Certificate for validate JWT issued by client and signed by Client private key from your keystore.</kc-tooltip></legend>
<div class="form-group" data-ng-hide="!signingKeyInfo.certificate"> <div class="form-group" data-ng-hide="!signingKeyInfo.certificate">
<label class="col-md-2 control-label" for="signingCert">Certificate</label> <label class="col-md-2 control-label" for="signingCert">Certificate</label>
@ -19,11 +19,11 @@
</div> </div>
</div> </div>
<div class="form-group" data-ng-show="!signingKeyInfo.certificate"> <div class="form-group" data-ng-show="!signingKeyInfo.certificate">
<label class="col-md-2 control-label" for="signingCert">Client Certificate not yet generated or imported!</label> <label class="col-md-4 control-label" for="signingCert">Client Certificate not yet generated or imported!</label>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients"> <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
<button class="btn btn-default" type="submit" data-ng-click="generateSigningKey()">Generate new keys</button> <button class="btn btn-default" type="submit" data-ng-click="generateSigningKey()">Generate new keys and certificate</button>
<button class="btn btn-default" type="submit" data-ng-click="importCertificate()">Import certificate</button> <button class="btn btn-default" type="submit" data-ng-click="importCertificate()">Import certificate</button>
<button class="btn btn-default" type="buttin" data-ng-click="cancel()">Cancel</button> <button class="btn btn-default" type="buttin" data-ng-click="cancel()">Cancel</button>
</div> </div>

View file

@ -29,7 +29,7 @@
</select> </select>
</div> </div>
</div> </div>
<kc-tooltip>What kind of top level flow is it? Type 'client' is used for authentication of clients (applications) when generic is for everything else</kc-tooltip> <kc-tooltip>What kind of top level flow is it? Type 'client' is used for authentication of clients (applications) when generic is for users and everything else</kc-tooltip>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-md-10 col-md-offset-2"> <div class="col-md-10 col-md-offset-2">

View file

@ -5,6 +5,7 @@ import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient; import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.adapters.authentication.ClientCredentialsProvider;
import org.keycloak.enums.RelativeUrlsUsed; import org.keycloak.enums.RelativeUrlsUsed;
import org.keycloak.enums.SslRequired; import org.keycloak.enums.SslRequired;
import org.keycloak.enums.TokenStore; import org.keycloak.enums.TokenStore;
@ -253,15 +254,25 @@ public class AdapterDeploymentContext {
} }
@Override @Override
public Map<String, String> getResourceCredentials() { public Map<String, Object> getResourceCredentials() {
return delegate.getResourceCredentials(); return delegate.getResourceCredentials();
} }
@Override @Override
public void setResourceCredentials(Map<String, String> resourceCredentials) { public void setResourceCredentials(Map<String, Object> resourceCredentials) {
delegate.setResourceCredentials(resourceCredentials); delegate.setResourceCredentials(resourceCredentials);
} }
@Override
public void setClientAuthenticator(ClientCredentialsProvider clientAuthenticator) {
delegate.setClientAuthenticator(clientAuthenticator);
}
@Override
public ClientCredentialsProvider getClientAuthenticator() {
return delegate.getClientAuthenticator();
}
@Override @Override
public HttpClient getClient() { public HttpClient getClient() {
return delegate.getClient(); return delegate.getClient();

View file

@ -9,6 +9,7 @@ import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.BasicAuthHelper;
@ -76,13 +77,7 @@ public class BasicAuthRequestAuthenticator extends BearerTokenRequestAuthenticat
formparams.add(new BasicNameValuePair("username", username)); formparams.add(new BasicNameValuePair("username", username));
formparams.add(new BasicNameValuePair("password", password)); formparams.add(new BasicNameValuePair("password", password));
if (deployment.isPublicClient()) { ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, deployment.getResourceName()));
} else {
String authorization = BasicAuthHelper.createHeader(deployment.getResourceName(),
deployment.getResourceCredentials().get("secret"));
post.setHeader("Authorization", authorization);
}
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form); post.setEntity(form);

View file

@ -1,50 +0,0 @@
package org.keycloak.adapters;
import java.io.InputStream;
import java.security.PrivateKey;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.util.FindFile;
import org.keycloak.util.KeystoreUtil;
import org.keycloak.util.Time;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientAuthAdapterUtils {
public static String createSignedJWT(KeycloakDeployment deployment) {
// TODO: Read all the config from KeycloakDeployment and call below
return null;
}
public static String createSignedJWT(String clientId, String realmInfoUrl,
String keystoreFile, String storePassword, String keyPassword, String alias, KeystoreUtil.KeystoreFormat type,
int tokenTimeout) {
JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl, tokenTimeout);
PrivateKey privateKey = KeystoreUtil.loadPrivateKeyFromKeystore(keystoreFile, storePassword, keyPassword, alias, type);
String signedToken = new JWSBuilder()
.jsonContent(jwt)
.rsa256(privateKey);
return signedToken;
}
private static JsonWebToken createRequestToken(String clientId, String realmInfoUrl, int tokenTimeout) {
JsonWebToken reqToken = new JsonWebToken();
reqToken.id(AdapterUtils.generateId());
reqToken.issuer(clientId);
reqToken.audience(realmInfoUrl);
int now = Time.currentTime();
reqToken.issuedAt(now);
reqToken.expiration(now + tokenTimeout);
reqToken.notBefore(now);
return reqToken;
}
}

View file

@ -2,6 +2,7 @@ package org.keycloak.adapters;
import org.apache.http.client.HttpClient; import org.apache.http.client.HttpClient;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.adapters.authentication.ClientCredentialsProvider;
import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.enums.RelativeUrlsUsed; import org.keycloak.enums.RelativeUrlsUsed;
import org.keycloak.enums.SslRequired; import org.keycloak.enums.SslRequired;
@ -39,7 +40,8 @@ public class KeycloakDeployment {
protected boolean bearerOnly; protected boolean bearerOnly;
protected boolean enableBasicAuth; protected boolean enableBasicAuth;
protected boolean publicClient; protected boolean publicClient;
protected Map<String, String> resourceCredentials = new HashMap<String, String>(); protected Map<String, Object> resourceCredentials = new HashMap<>();
protected ClientCredentialsProvider clientAuthenticator;
protected HttpClient client; protected HttpClient client;
protected String scope; protected String scope;
@ -216,14 +218,22 @@ public class KeycloakDeployment {
this.publicClient = publicClient; this.publicClient = publicClient;
} }
public Map<String, String> getResourceCredentials() { public Map<String, Object> getResourceCredentials() {
return resourceCredentials; return resourceCredentials;
} }
public void setResourceCredentials(Map<String, String> resourceCredentials) { public void setResourceCredentials(Map<String, Object> resourceCredentials) {
this.resourceCredentials = resourceCredentials; this.resourceCredentials = resourceCredentials;
} }
public ClientCredentialsProvider getClientAuthenticator() {
return clientAuthenticator;
}
public void setClientAuthenticator(ClientCredentialsProvider clientAuthenticator) {
this.clientAuthenticator = clientAuthenticator;
}
public HttpClient getClient() { public HttpClient getClient() {
return client; return client;
} }

View file

@ -3,6 +3,7 @@ package org.keycloak.adapters;
import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.annotate.JsonSerialize; import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.enums.SslRequired; import org.keycloak.enums.SslRequired;
import org.keycloak.enums.TokenStore; import org.keycloak.enums.TokenStore;
import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.representations.adapters.config.AdapterConfig;
@ -55,7 +56,10 @@ public class KeycloakDeploymentBuilder {
deployment.setTokenStore(TokenStore.SESSION); deployment.setTokenStore(TokenStore.SESSION);
} }
if (adapterConfig.getPrincipalAttribute() != null) deployment.setPrincipalAttribute(adapterConfig.getPrincipalAttribute()); if (adapterConfig.getPrincipalAttribute() != null) deployment.setPrincipalAttribute(adapterConfig.getPrincipalAttribute());
deployment.setResourceCredentials(adapterConfig.getCredentials()); deployment.setResourceCredentials(adapterConfig.getCredentials());
deployment.setClientAuthenticator(ClientCredentialsProviderUtils.bootstrapClientAuthenticator(deployment));
deployment.setPublicClient(adapterConfig.isPublicClient()); deployment.setPublicClient(adapterConfig.isPublicClient());
deployment.setUseResourceRoleMappings(adapterConfig.isUseResourceRoleMappings()); deployment.setUseResourceRoleMappings(adapterConfig.isUseResourceRoleMappings());

View file

@ -8,10 +8,9 @@ import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.constants.AdapterConstants; import org.keycloak.constants.AdapterConstants;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.HostUtils; import org.keycloak.util.HostUtils;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.util.KeycloakUriBuilder; import org.keycloak.util.KeycloakUriBuilder;
@ -23,7 +22,6 @@ import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -50,30 +48,17 @@ public class ServerRequest {
} }
public static void invokeLogout(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure { public static void invokeLogout(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure {
String client_id = deployment.getResourceName();
Map<String, String> credentials = deployment.getResourceCredentials();
HttpClient client = deployment.getClient(); HttpClient client = deployment.getClient();
URI uri = deployment.getLogoutUrl().clone().build(); URI uri = deployment.getLogoutUrl().clone().build();
List<NameValuePair> formparams = new ArrayList<NameValuePair>(); List<NameValuePair> formparams = new ArrayList<>();
for (Map.Entry<String, String> entry : credentials.entrySet()) {
formparams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken)); formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
HttpResponse response = null;
HttpPost post = new HttpPost(uri); HttpPost post = new HttpPost(uri);
if (!deployment.isPublicClient()) { ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
String clientSecret = credentials.get(CredentialRepresentation.SECRET);
if (clientSecret != null) {
String authorization = BasicAuthHelper.createHeader(client_id, clientSecret);
post.setHeader("Authorization", authorization);
}
} else {
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, client_id));
}
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form); post.setEntity(form);
response = client.execute(post); HttpResponse response = client.execute(post);
int status = response.getStatusLine().getStatusCode(); int status = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity(); HttpEntity entity = response.getEntity();
if (status != 204) { if (status != 204) {
@ -86,17 +71,8 @@ public class ServerRequest {
if (is != null) is.close(); if (is != null) is.close();
} }
public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri, String sessionId) throws HttpFailure, IOException { public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri, String sessionId) throws IOException, HttpFailure {
String tokenUrl = deployment.getTokenUrl(); List<NameValuePair> formparams = new ArrayList<>();
String client_id = deployment.getResourceName();
Map<String, String> credentials = deployment.getResourceCredentials();
HttpClient client = deployment.getClient();
return invokeAccessCodeToToken(client, deployment.isPublicClient(), code, tokenUrl, redirectUri, client_id, credentials, sessionId);
}
public static AccessTokenResponse invokeAccessCodeToToken(HttpClient client, boolean publicClient, String code, String tokenUrl, String redirectUri, String client_id, Map<String, String> credentials, String sessionId) throws IOException, HttpFailure {
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
redirectUri = stripOauthParametersFromRedirect(redirectUri); redirectUri = stripOauthParametersFromRedirect(redirectUri);
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "authorization_code")); formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "authorization_code"));
formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code)); formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
@ -105,21 +81,13 @@ public class ServerRequest {
formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, sessionId)); formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, sessionId));
formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, HostUtils.getHostName())); formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, HostUtils.getHostName()));
} }
HttpResponse response = null;
HttpPost post = new HttpPost(tokenUrl); HttpPost post = new HttpPost(deployment.getTokenUrl());
if (!publicClient) { ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
String clientSecret = credentials.get(CredentialRepresentation.SECRET);
if (clientSecret != null) {
String authorization = BasicAuthHelper.createHeader(client_id, clientSecret);
post.setHeader("Authorization", authorization);
}
} else {
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, client_id));
}
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form); post.setEntity(form);
response = client.execute(post); HttpResponse response = deployment.getClient().execute(post);
int status = response.getStatusLine().getStatusCode(); int status = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity(); HttpEntity entity = response.getEntity();
if (status != 200) { if (status != 200) {
@ -152,36 +120,16 @@ public class ServerRequest {
} }
public static AccessTokenResponse invokeRefresh(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure { public static AccessTokenResponse invokeRefresh(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure {
String tokenUrl = deployment.getTokenUrl();
String client_id = deployment.getResourceName();
Map<String, String> credentials = deployment.getResourceCredentials();
HttpClient client = deployment.getClient();
return invokeRefresh(client, deployment.isPublicClient(), refreshToken, tokenUrl, client_id, credentials);
}
public static AccessTokenResponse invokeRefresh(HttpClient client, boolean publicClient, String refreshToken, String tokenUrl, String client_id, Map<String, String> credentials) throws IOException, HttpFailure {
List<NameValuePair> formparams = new ArrayList<NameValuePair>(); List<NameValuePair> formparams = new ArrayList<NameValuePair>();
for (Map.Entry<String, String> entry : credentials.entrySet()) {
formparams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN)); formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken)); formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
HttpResponse response = null;
HttpPost post = new HttpPost(tokenUrl); HttpPost post = new HttpPost(deployment.getTokenUrl());
if (!publicClient) { ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
String clientSecret = credentials.get(CredentialRepresentation.SECRET);
if (clientSecret != null) {
String authorization = BasicAuthHelper.createHeader(client_id, clientSecret);
post.setHeader("Authorization", authorization);
}
} else {
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, client_id));
}
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form); post.setEntity(form);
response = client.execute(post); HttpResponse response = deployment.getClient().execute(post);
int status = response.getStatusLine().getStatusCode(); int status = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity(); HttpEntity entity = response.getEntity();
if (status != 200) { if (status != 200) {
@ -215,43 +163,28 @@ public class ServerRequest {
public static void invokeRegisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException { public static void invokeRegisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException {
String registerNodeUrl = deployment.getRegisterNodeUrl(); String registerNodeUrl = deployment.getRegisterNodeUrl();
String client_id = deployment.getResourceName(); invokeClientManagementRequest(deployment, host, registerNodeUrl);
Map<String, String> credentials = deployment.getResourceCredentials();
HttpClient client = deployment.getClient();
invokeClientManagementRequest(client, host, registerNodeUrl, client_id, credentials);
} }
public static void invokeUnregisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException { public static void invokeUnregisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException {
String unregisterNodeUrl = deployment.getUnregisterNodeUrl(); String unregisterNodeUrl = deployment.getUnregisterNodeUrl();
String client_id = deployment.getResourceName(); invokeClientManagementRequest(deployment, host, unregisterNodeUrl);
Map<String, String> credentials = deployment.getResourceCredentials();
HttpClient client = deployment.getClient();
invokeClientManagementRequest(client, host, unregisterNodeUrl, client_id, credentials);
} }
public static void invokeClientManagementRequest(HttpClient client, String host, String endpointUrl, String clientId, Map<String, String> credentials) throws HttpFailure, IOException { public static void invokeClientManagementRequest(KeycloakDeployment deployment, String host, String endpointUrl) throws HttpFailure, IOException {
if (endpointUrl == null) { if (endpointUrl == null) {
throw new IOException("You need to configure URI for register/unregister node for application " + clientId); throw new IOException("You need to configure URI for register/unregister node for application " + deployment.getResourceName());
} }
List<NameValuePair> formparams = new ArrayList<NameValuePair>(); List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_CLUSTER_HOST, host)); formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_CLUSTER_HOST, host));
HttpPost post = new HttpPost(endpointUrl); HttpPost post = new HttpPost(endpointUrl);
ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
String clientSecret = credentials.get(CredentialRepresentation.SECRET);
if (clientSecret != null) {
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
post.setHeader("Authorization", authorization);
} else {
throw new IOException("You need to configure clientSecret for register/unregister node for application " + clientId);
}
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form); post.setEntity(form);
HttpResponse response = client.execute(post); HttpResponse response = deployment.getClient().execute(post);
int status = response.getStatusLine().getStatusCode(); int status = response.getStatusLine().getStatusCode();
if (status != 204) { if (status != 204) {
HttpEntity entity = response.getEntity(); HttpEntity entity = response.getEntity();

View file

@ -0,0 +1,19 @@
package org.keycloak.adapters.authentication;
import java.util.Map;
import org.keycloak.adapters.KeycloakDeployment;
/**
* TODO: Javadoc
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface ClientCredentialsProvider {
String getId();
void init(KeycloakDeployment deployment, Object config);
void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams);
}

View file

@ -0,0 +1,85 @@
package org.keycloak.adapters.authentication;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import org.apache.http.NameValuePair;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.jboss.logging.Logger;
import org.keycloak.adapters.KeycloakDeployment;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientCredentialsProviderUtils {
private static Logger logger = Logger.getLogger(ClientCredentialsProviderUtils.class);
public static ClientCredentialsProvider bootstrapClientAuthenticator(KeycloakDeployment deployment) {
String clientId = deployment.getResourceName();
Map<String, Object> clientCredentials = deployment.getResourceCredentials();
String authenticatorId;
if (clientCredentials == null || clientCredentials.isEmpty()) {
authenticatorId = ClientIdAndSecretCredentialsProvider.PROVIDER_ID;
} else {
authenticatorId = (String) clientCredentials.get("provider");
if (authenticatorId == null) {
// If there is just one credential type, use provider from it
if (clientCredentials.size() == 1) {
authenticatorId = clientCredentials.keySet().iterator().next();
} else {
throw new RuntimeException("Can't identify clientAuthenticator from the configuration of client '" + clientId + "' . Check your adapter configurations");
}
}
}
logger.debugf("Using provider '%s' for authentication of client '%s'", authenticatorId, clientId);
Map<String, ClientCredentialsProvider> authenticators = new HashMap<>();
loadAuthenticators(authenticators, ClientCredentialsProviderUtils.class.getClassLoader());
loadAuthenticators(authenticators, Thread.currentThread().getContextClassLoader());
ClientCredentialsProvider authenticator = authenticators.get(authenticatorId);
if (authenticator == null) {
throw new RuntimeException("Couldn't find ClientCredentialsProvider implementation class with id: " + authenticatorId + ". Loaded authentication providers: " + authenticators.keySet());
}
Object config = (clientCredentials==null) ? null : clientCredentials.get(authenticatorId);
authenticator.init(deployment, config);
return authenticator;
}
private static void loadAuthenticators(Map<String, ClientCredentialsProvider> authenticators, ClassLoader classLoader) {
for (ClientCredentialsProvider authenticator : ServiceLoader.load(ClientCredentialsProvider.class, classLoader)) {
authenticators.put(authenticator.getId(), authenticator);
}
}
public static void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formparams) {
ClientCredentialsProvider authenticator = deployment.getClientAuthenticator();
authenticator.setClientCredentials(deployment, requestHeaders, formparams);
}
/**
* Don't use directly from your JEE apps to avoid HttpClient linkage errors! Instead use the method {@link #setClientCredentials(KeycloakDeployment, Map, Map)}
*/
public static void setClientCredentials(KeycloakDeployment deployment, HttpPost post, List<NameValuePair> formparams) {
Map<String, String> reqHeaders = new HashMap<>();
Map<String, String> reqParams = new HashMap<>();
setClientCredentials(deployment, reqHeaders, reqParams);
for (Map.Entry<String, String> header : reqHeaders.entrySet()) {
post.setHeader(header.getKey(), header.getValue());
}
for (Map.Entry<String, String> param : reqParams.entrySet()) {
formparams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
}
}

View file

@ -0,0 +1,49 @@
package org.keycloak.adapters.authentication;
import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.util.BasicAuthHelper;
/**
* Traditional OAuth2 authentication of clients based on client_id and client_secret
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientIdAndSecretCredentialsProvider implements ClientCredentialsProvider {
private static Logger logger = Logger.getLogger(ClientCredentialsProviderUtils.class);
public static final String PROVIDER_ID = CredentialRepresentation.SECRET;
private String clientSecret;
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public void init(KeycloakDeployment deployment, Object config) {
clientSecret = (String) config;
}
@Override
public void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
String clientId = deployment.getResourceName();
if (!deployment.isPublicClient()) {
if (clientSecret != null) {
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
requestHeaders.put("Authorization", authorization);
} else {
logger.warnf("Client '%s' doesn't have secret available", clientId);
}
} else {
formParams.put(OAuth2Constants.CLIENT_ID, clientId);
}
}
}

View file

@ -0,0 +1,103 @@
package org.keycloak.adapters.authentication;
import java.security.PrivateKey;
import java.util.Map;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.util.KeystoreUtil;
import org.keycloak.util.Time;
/**
* Client authentication based on JWT signed by client private key
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
public static final String PROVIDER_ID = "jwt";
private PrivateKey privateKey;
private int tokenTimeout;
@Override
public String getId() {
return PROVIDER_ID;
}
public void setPrivateKey(PrivateKey privateKey) {
this.privateKey = privateKey;
}
public void setTokenTimeout(int tokenTimeout) {
this.tokenTimeout = tokenTimeout;
}
@Override
public void init(KeycloakDeployment deployment, Object config) {
if (config == null || !(config instanceof Map)) {
throw new RuntimeException("Configuration of jwt credentials is missing or incorrect for client '" + deployment.getResourceName() + "'. Check your adapter configuration");
}
Map<String, Object> cfg = (Map<String, Object>) config;
String clientKeystoreFile = (String) cfg.get("client-keystore-file");
if (clientKeystoreFile == null) {
throw new RuntimeException("Missing parameter client-keystore-file in configuration of jwt for client " + deployment.getResourceName());
}
String clientKeystoreType = (String) cfg.get("client-keystore-type");
KeystoreUtil.KeystoreFormat clientKeystoreFormat = clientKeystoreType==null ? KeystoreUtil.KeystoreFormat.JKS : Enum.valueOf(KeystoreUtil.KeystoreFormat.class, clientKeystoreType);
String clientKeystorePassword = (String) cfg.get("client-keystore-password");
if (clientKeystorePassword == null) {
throw new RuntimeException("Missing parameter client-keystore-password in configuration of jwt for client " + deployment.getResourceName());
}
String clientKeyPassword = (String) cfg.get("client-key-password");
if (clientKeyPassword == null) {
clientKeyPassword = clientKeystorePassword;
}
String clientKeyAlias = (String) cfg.get("client-key-alias");
if (clientKeyAlias == null) {
clientKeyAlias = deployment.getResourceName();
}
this.privateKey = KeystoreUtil.loadPrivateKeyFromKeystore(clientKeystoreFile, clientKeystorePassword, clientKeyPassword, clientKeyAlias, clientKeystoreFormat);
Integer tokenExp = (Integer) cfg.get("token-timeout");
this.tokenTimeout = (tokenExp==null) ? 10 : tokenExp;
}
@Override
public void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
String signedToken = createSignedRequestToken(deployment.getResourceName(), deployment.getRealmInfoUrl());
formParams.put(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT);
formParams.put(OAuth2Constants.CLIENT_ASSERTION, signedToken);
}
public String createSignedRequestToken(String clientId, String realmInfoUrl) {
JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl);
return new JWSBuilder()
.jsonContent(jwt)
.rsa256(privateKey);
}
protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
JsonWebToken reqToken = new JsonWebToken();
reqToken.id(AdapterUtils.generateId());
reqToken.issuer(clientId);
reqToken.audience(realmInfoUrl);
int now = Time.currentTime();
reqToken.issuedAt(now);
reqToken.expiration(now + this.tokenTimeout);
reqToken.notBefore(now);
return reqToken;
}
}

View file

@ -23,6 +23,7 @@ import org.apache.http.message.BasicNameValuePair;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.VerificationException; import org.keycloak.VerificationException;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.BasicAuthHelper;
@ -72,14 +73,8 @@ public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule {
formparams.add(new BasicNameValuePair("username", username)); formparams.add(new BasicNameValuePair("username", username));
formparams.add(new BasicNameValuePair("password", password)); formparams.add(new BasicNameValuePair("password", password));
if (deployment.isPublicClient()) { // if client is public access type ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, deployment.getResourceName()));
} else {
String clientId = deployment.getResourceName();
String clientSecret = deployment.getResourceCredentials().get("secret");
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
post.setHeader("Authorization", authorization);
}
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form); post.setEntity(form);
@ -135,15 +130,7 @@ public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule {
HttpPost post = new HttpPost(logoutUri); HttpPost post = new HttpPost(logoutUri);
List<NameValuePair> formparams = new ArrayList<NameValuePair>(); List<NameValuePair> formparams = new ArrayList<NameValuePair>();
if (deployment.isPublicClient()) { // if client is public access type ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, deployment.getResourceName()));
} else {
String clientId = deployment.getResourceName();
String clientSecret = deployment.getResourceCredentials().get("secret");
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
post.setHeader("Authorization", authorization);
}
formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken)); formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");

View file

@ -0,0 +1,2 @@
org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider
org.keycloak.adapters.authentication.JWTClientCredentialsProvider

View file

@ -2,6 +2,9 @@ package org.keycloak.adapters;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.junit.Test; import org.junit.Test;
import org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider;
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
import org.keycloak.enums.RelativeUrlsUsed;
import org.keycloak.enums.SslRequired; import org.keycloak.enums.SslRequired;
import org.keycloak.enums.TokenStore; import org.keycloak.enums.TokenStore;
import org.keycloak.util.PemUtils; import org.keycloak.util.PemUtils;
@ -32,8 +35,10 @@ public class KeycloakDeploymentBuilderTest {
assertTrue(deployment.isEnableBasicAuth()); assertTrue(deployment.isEnableBasicAuth());
assertTrue(deployment.isExposeToken()); assertTrue(deployment.isExposeToken());
assertEquals("234234-234234-234234", deployment.getResourceCredentials().get("secret")); assertEquals("234234-234234-234234", deployment.getResourceCredentials().get("secret"));
assertEquals(ClientIdAndSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
assertEquals(20, ((ThreadSafeClientConnManager) deployment.getClient().getConnectionManager()).getMaxTotal()); assertEquals(20, ((ThreadSafeClientConnManager) deployment.getClient().getConnectionManager()).getMaxTotal());
assertEquals("https://backend:8443/auth/realms/demo/protocol/openid-connect/token", deployment.getTokenUrl()); assertEquals("https://backend:8443/auth/realms/demo/protocol/openid-connect/token", deployment.getTokenUrl());
assertEquals(RelativeUrlsUsed.NEVER, deployment.getRelativeUrls());
assertTrue(deployment.isAlwaysRefreshToken()); assertTrue(deployment.isAlwaysRefreshToken());
assertTrue(deployment.isRegisterNodeAtStartup()); assertTrue(deployment.isRegisterNodeAtStartup());
assertEquals(1000, deployment.getRegisterNodePeriod()); assertEquals(1000, deployment.getRegisterNodePeriod());
@ -41,4 +46,16 @@ public class KeycloakDeploymentBuilderTest {
assertEquals("email", deployment.getPrincipalAttribute()); assertEquals("email", deployment.getPrincipalAttribute());
} }
@Test
public void loadNoClientCredentials() throws Exception {
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-no-credentials.json"));
assertEquals(ClientIdAndSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
}
@Test
public void loadJwtCredentials() throws Exception {
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-jwt.json"));
assertEquals(JWTClientCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
}
} }

View file

@ -0,0 +1,13 @@
{
"realm": "demo",
"resource": "customer-portal",
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url": "https://localhost:8443/auth",
"ssl-required": "external",
"credentials": {
"jwt": {
"client-keystore-file": "classpath:keystore.jks",
"client-keystore-password": "storepass"
}
}
}

View file

@ -0,0 +1,8 @@
{
"realm": "demo",
"resource": "customer-portal",
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url": "https://localhost:8443/auth",
"public-client": true,
"expose-token": true
}

View file

@ -22,8 +22,8 @@
"truststore": "classpath:/cacerts.jks", "truststore": "classpath:/cacerts.jks",
"truststore-password": "changeit", "truststore-password": "changeit",
"client-keystore": "classpath:/keystore.jks", "client-keystore": "classpath:/keystore.jks",
"client-keystore-password": "changeit", "client-keystore-password": "storepass",
"client-key-password": "password", "client-key-password": "keypass",
"auth-server-url-for-backend-requests": "https://backend:8443/auth", "auth-server-url-for-backend-requests": "https://backend:8443/auth",
"always-refresh-token": true, "always-refresh-token": true,
"register-node-at-startup": true, "register-node-at-startup": true,

View file

@ -50,8 +50,8 @@ public class JaxrsOAuthClient extends AbstractOAuthClient {
.param(OAuth2Constants.CODE, code) .param(OAuth2Constants.CODE, code)
.param(OAuth2Constants.CLIENT_ID, clientId) .param(OAuth2Constants.CLIENT_ID, clientId)
.param(OAuth2Constants.REDIRECT_URI, redirectUri); .param(OAuth2Constants.REDIRECT_URI, redirectUri);
for (Map.Entry<String, String> entry : credentials.entrySet()) { for (Map.Entry<String, Object> entry : credentials.entrySet()) {
codeForm.param(entry.getKey(), entry.getValue()); codeForm.param(entry.getKey(), (String) entry.getValue());
} }
Response res = client.target(tokenUrl).request().post(Entity.form(codeForm)); Response res = client.target(tokenUrl).request().post(Entity.form(codeForm));
try { try {

View file

@ -44,6 +44,27 @@
<artifactId>jboss-servlet-api_3.0_spec</artifactId> <artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-xc</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>${jboss.logging.version}</version>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>

View file

@ -0,0 +1,83 @@
package org.keycloak.servlet;
import java.util.Map;
import org.keycloak.AbstractOAuthClient;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.enums.RelativeUrlsUsed;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KeycloakDeploymentDelegateOAuthClient extends AbstractOAuthClient {
private KeycloakDeployment deployment;
public KeycloakDeployment getDeployment() {
return deployment;
}
public void setDeployment(KeycloakDeployment deployment) {
this.deployment = deployment;
}
@Override
public String getClientId() {
return deployment.getResourceName();
}
@Override
public void setClientId(String clientId) {
deployment.setResourceName(clientId);
}
@Override
public Map<String, Object> getCredentials() {
return deployment.getResourceCredentials();
}
@Override
public void setCredentials(Map<String, Object> credentials) {
deployment.setResourceCredentials(credentials);
}
@Override
public String getAuthUrl() {
return deployment.getAuthUrl().clone().build().toString();
}
@Override
public void setAuthUrl(String authUrl) {
throw new IllegalStateException("Illegal to call this method");
}
@Override
public String getTokenUrl() {
return deployment.getTokenUrl();
}
@Override
public void setTokenUrl(String tokenUrl) {
throw new IllegalStateException("Illegal to call this method");
}
@Override
public boolean isPublicClient() {
return deployment.isPublicClient();
}
@Override
public void setPublicClient(boolean publicClient) {
deployment.setPublicClient(publicClient);
}
@Override
public RelativeUrlsUsed getRelativeUrlsUsed() {
return deployment.getRelativeUrls();
}
@Override
public void setRelativeUrlsUsed(RelativeUrlsUsed relativeUrlsUsed) {
throw new IllegalStateException("Illegal to call this method");
}
}

View file

@ -1,14 +1,10 @@
package org.keycloak.servlet; package org.keycloak.servlet;
import org.apache.http.client.HttpClient;
import org.keycloak.AbstractOAuthClient;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.adapters.ServerRequest; import org.keycloak.adapters.ServerRequest;
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.representations.adapters.config.AdapterConfig;
import org.keycloak.util.KeycloakUriBuilder; import org.keycloak.util.KeycloakUriBuilder;
import org.keycloak.util.UriUtils; import org.keycloak.util.UriUtils;
@ -22,24 +18,18 @@ import java.net.URI;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class ServletOAuthClient extends AbstractOAuthClient { public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
protected HttpClient client;
protected AdapterConfig adapterConfig;
public void start() {
client = new HttpClientBuilder().build(adapterConfig);
}
/** /**
* closes client * closes client
*/ */
public void stop() { public void stop() {
client.getConnectionManager().shutdown(); getDeployment().getClient().getConnectionManager().shutdown();
} }
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(client, publicClient, code, getUrl(request, tokenUrl, false), redirectUri, clientId, credentials, null); return ServerRequest.invokeAccessCodeToToken(getDeployment(), code, redirectUri, null);
} }
/** /**
@ -146,7 +136,7 @@ public class ServletOAuthClient extends AbstractOAuthClient {
} }
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(client, publicClient, refreshToken, getUrl(request, tokenUrl, false), clientId, credentials); return ServerRequest.invokeRefresh(getDeployment(), refreshToken);
} }
public static IDToken extractIdToken(String idToken) { public static IDToken extractIdToken(String idToken) {
@ -167,8 +157,4 @@ public class ServletOAuthClient extends AbstractOAuthClient {
return url; return url;
} }
} }
public void setAdapterConfig(AdapterConfig adapterConfig) {
this.adapterConfig = adapterConfig;
}
} }

View file

@ -1,14 +1,8 @@
package org.keycloak.servlet; package org.keycloak.servlet;
import org.apache.http.client.HttpClient; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.enums.RelativeUrlsUsed;
import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.JsonSerialization;
import org.keycloak.util.KeycloakUriBuilder;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
/** /**
@ -18,62 +12,21 @@ import java.io.InputStream;
public class ServletOAuthClientBuilder { public class ServletOAuthClientBuilder {
public static ServletOAuthClient build(InputStream is) { public static ServletOAuthClient build(InputStream is) {
AdapterConfig adapterConfig = getAdapterConfig(is); KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(is);
return build(adapterConfig); ServletOAuthClient client = new ServletOAuthClient();
} client.setDeployment(deployment);
return client;
public static AdapterConfig getAdapterConfig(InputStream is) {
try {
return JsonSerialization.readValue(is, AdapterConfig.class, true);
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
public static ServletOAuthClient build(AdapterConfig adapterConfig) { public static ServletOAuthClient build(AdapterConfig adapterConfig) {
ServletOAuthClient oauthClient = new ServletOAuthClient(); KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(adapterConfig);
build(adapterConfig, oauthClient); ServletOAuthClient client = new ServletOAuthClient();
return oauthClient; client.setDeployment(deployment);
return client;
} }
public static void build(InputStream is, ServletOAuthClient oauthClient) { public static void build(InputStream is, ServletOAuthClient oauthClient) {
build(getAdapterConfig(is), oauthClient); KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(is);
} oauthClient.setDeployment(deployment);
public static void build(AdapterConfig adapterConfig, ServletOAuthClient oauthClient) {
oauthClient.setAdapterConfig(adapterConfig);
oauthClient.setClientId(adapterConfig.getResource());
oauthClient.setPublicClient(adapterConfig.isPublicClient());
oauthClient.setCredentials(adapterConfig.getCredentials());
if (adapterConfig.getAuthServerUrl() == null) {
throw new RuntimeException("You must specify auth-url");
}
KeycloakUriBuilder serverBuilder = KeycloakUriBuilder.fromUri(adapterConfig.getAuthServerUrl());
RelativeUrlsUsed useRelative = relativeUrls(serverBuilder, adapterConfig);
oauthClient.setRelativeUrlsUsed(useRelative);
String authUrl = serverBuilder.clone().path(ServiceUrlConstants.AUTH_PATH).build(adapterConfig.getRealm()).toString();
KeycloakUriBuilder tokenUrlBuilder;
if (useRelative == RelativeUrlsUsed.BROWSER_ONLY) {
// Use absolute URI for refreshToken and codeToToken requests
KeycloakUriBuilder nonBrowsersServerBuilder = KeycloakUriBuilder.fromUri(adapterConfig.getAuthServerUrlForBackendRequests());
tokenUrlBuilder = nonBrowsersServerBuilder.clone();
} else {
tokenUrlBuilder = serverBuilder.clone();
}
String tokenUrl = tokenUrlBuilder.path(ServiceUrlConstants.TOKEN_PATH).build(adapterConfig.getRealm()).toString();
oauthClient.setAuthUrl(authUrl);
oauthClient.setTokenUrl(tokenUrl);
}
private static RelativeUrlsUsed relativeUrls(KeycloakUriBuilder serverBuilder, AdapterConfig adapterConfig) {
if (serverBuilder.clone().getHost() == null) {
return (adapterConfig.getAuthServerUrlForBackendRequests() != null) ? RelativeUrlsUsed.BROWSER_ONLY : RelativeUrlsUsed.ALL_REQUESTS;
} else {
return RelativeUrlsUsed.NEVER;
}
} }
} }

View file

@ -0,0 +1,25 @@
package org.keycloak.servlet;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.enums.RelativeUrlsUsed;
import org.keycloak.representations.idm.CredentialRepresentation;
import static org.junit.Assert.assertEquals;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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());
assertEquals(RelativeUrlsUsed.NEVER, oauthClient.getRelativeUrlsUsed());
Assert.assertEquals("customer-portal", oauthClient.getClientId());
Assert.assertEquals("234234-234234-234234", oauthClient.getCredentials().get(CredentialRepresentation.SECRET));
Assert.assertEquals(true, oauthClient.isPublicClient());
}
}

View file

@ -0,0 +1,28 @@
{
"realm": "demo",
"resource": "customer-portal",
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url": "https://localhost:8443/auth",
"ssl-required": "external",
"use-resource-role-mappings": true,
"enable-cors": true,
"cors-max-age": 1000,
"cors-allowed-methods": "POST, PUT, DELETE, GET",
"cors-allowed-headers": "X-Custom, X-Custom2",
"bearer-only": true,
"public-client": true,
"enable-basic-auth": true,
"expose-token": true,
"credentials": {
"secret": "234234-234234-234234"
},
"connection-pool-size": 20,
"disable-trust-manager": true,
"allow-any-hostname": true,
"auth-server-url-for-backend-requests": "https://backend:8443/auth",
"always-refresh-token": true,
"register-node-at-startup": true,
"register-node-period": 1000,
"token-store": "cookie",
"principal-attribute": "email"
}

View file

@ -304,7 +304,7 @@ public class DefaultAuthenticationFlows {
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(clients.getId()); execution.setParentFlow(clients.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
execution.setAuthenticator("client-signed-jwt"); execution.setAuthenticator("client-jwt");
execution.setPriority(20); execution.setPriority(20);
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);

View file

@ -33,6 +33,7 @@ import org.keycloak.util.Time;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.net.URI; import java.net.URI;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -67,6 +68,7 @@ public class AuthenticationProcessor {
// Used for client authentication // Used for client authentication
protected ClientModel client; protected ClientModel client;
protected Map<String, String> clientAuthAttributes = new HashMap<>();
public AuthenticationProcessor() { public AuthenticationProcessor() {
} }
@ -83,6 +85,10 @@ public class AuthenticationProcessor {
this.client = client; this.client = client;
} }
public Map<String, String> getClientAuthAttributes() {
return clientAuthAttributes;
}
public ClientSessionModel getClientSession() { public ClientSessionModel getClientSession() {
return clientSession; return clientSession;
} }
@ -341,6 +347,11 @@ public class AuthenticationProcessor {
AuthenticationProcessor.this.setClient(client); AuthenticationProcessor.this.setClient(client);
} }
@Override
public Map<String, String> getClientAuthAttributes() {
return AuthenticationProcessor.this.getClientAuthAttributes();
}
@Override @Override
public ClientSessionModel getClientSession() { public ClientSessionModel getClientSession() {
return AuthenticationProcessor.this.getClientSession(); return AuthenticationProcessor.this.getClientSession();

View file

@ -55,20 +55,6 @@ public class ClientAuthenticationFlow implements AuthenticationFlow {
AuthenticationFlow authenticationFlow; AuthenticationFlow authenticationFlow;
authenticationFlow = processor.createFlowExecution(model.getFlowId(), model); authenticationFlow = processor.createFlowExecution(model.getFlowId(), model);
/*if (model.getFlowId() != null) {
authenticationFlow = processor.createFlowExecution(model.getFlowId(), model);
} else {
// Continue with the flow specific to authenticatedClient
ClientModel authenticatedClient = processor.getClient();
if (authenticatedClient != null) {
String clientFlowId = authenticatedClient.getClientAuthFlowId();
authenticationFlow = processor.createFlowExecution(clientFlowId, model);
} else {
throw new AuthenticationFlowException("Authenticated client required for: " + model.getAuthenticator(), AuthenticationFlowError.CLIENT_NOT_FOUND);
}
}*/
Response flowChallenge = authenticationFlow.processFlow(); Response flowChallenge = authenticationFlow.processFlow();
if (flowChallenge == null) { if (flowChallenge == null) {
if (model.isAlternative()) alternativeSuccessful = true; if (model.isAlternative()) alternativeSuccessful = true;

View file

@ -1,5 +1,7 @@
package org.keycloak.authentication; package org.keycloak.authentication;
import java.util.Map;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
/** /**
@ -22,4 +24,15 @@ public interface ClientAuthenticationFlowContext extends AbstractAuthenticationF
*/ */
void setClient(ClientModel client); void setClient(ClientModel client);
/**
* Return the map where the authenticators can put some additional state related to authenticated client and the context how was
* client authenticated (ie. attributes from client certificate etc). Map is writable, so you can add/remove items from it as needed.
*
* After successful authentication will be those state data put into UserSession notes. This allows you to configure
* UserSessionNote protocol mapper for your client, which will allow to map those state data into the access token available in the application
*
* @return
*/
Map<String, String> getClientAuthAttributes();
} }

View file

@ -29,51 +29,4 @@ public class ClientAuthUtil {
return Response.status(status).entity(e).type(MediaType.APPLICATION_JSON_TYPE).build(); return Response.status(status).entity(e).type(MediaType.APPLICATION_JSON_TYPE).build();
} }
// Return client either from client_id parameter or from "username" send in "Authorization: Basic" header.
public static ClientModel getClientFromClientId(ClientAuthenticationFlowContext context) {
String client_id = null;
String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
if (authorizationHeader != null) {
String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
if (usernameSecret != null) {
client_id = usernameSecret[0];
} else {
// Don't send 401 if client_id parameter was sent in request. For example IE may automatically send "Authorization: Negotiate" in XHR requests even for public clients
if (!formData.containsKey(OAuth2Constants.CLIENT_ID)) {
Response challengeResponse = Response.status(Response.Status.UNAUTHORIZED).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + context.getRealm().getName() + "\"").build();
context.challenge(challengeResponse);
return null;
}
}
}
if (client_id == null) {
client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
}
if (client_id == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Missing client_id parameter");
context.challenge(challengeResponse);
return null;
}
context.getEvent().client(client_id);
ClientModel client = context.getRealm().getClientByClientId(client_id);
if (client == null) {
context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND, null);
return null;
}
if (!client.isEnabled()) {
context.failure(AuthenticationFlowError.CLIENT_DISABLED, null);
return null;
}
return client;
}
} }

View file

@ -8,6 +8,7 @@ import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext; import org.keycloak.authentication.ClientAuthenticationFlowContext;
@ -39,21 +40,60 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
@Override @Override
public void authenticateClient(ClientAuthenticationFlowContext context) { public void authenticateClient(ClientAuthenticationFlowContext context) {
ClientModel client = ClientAuthUtil.getClientFromClientId(context); String client_id = null;
if (client == null) { String clientSecret = null;
return;
} else { String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
context.setClient(client); MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
if (authorizationHeader != null) {
String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
if (usernameSecret != null) {
client_id = usernameSecret[0];
clientSecret = usernameSecret[1];
} else {
// Don't send 401 if client_id parameter was sent in request. For example IE may automatically send "Authorization: Negotiate" in XHR requests even for public clients
if (!formData.containsKey(OAuth2Constants.CLIENT_ID)) {
Response challengeResponse = Response.status(Response.Status.UNAUTHORIZED).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + context.getRealm().getName() + "\"").build();
context.challenge(challengeResponse);
return;
}
}
} }
if (client_id == null) {
client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
clientSecret = formData.getFirst("client_secret");
}
if (client_id == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Missing client_id parameter");
context.challenge(challengeResponse);
return;
}
context.getEvent().client(client_id);
ClientModel client = context.getRealm().getClientByClientId(client_id);
if (client == null) {
context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND, null);
return;
}
if (!client.isEnabled()) {
context.failure(AuthenticationFlowError.CLIENT_DISABLED, null);
return;
}
context.setClient(client);
// Skip client_secret validation for public client // Skip client_secret validation for public client
if (client.isPublicClient()) { if (client.isPublicClient()) {
context.success(); context.success();
return; return;
} }
String clientSecret = getClientSecret(context);
if (clientSecret == null) { if (clientSecret == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client secret not provided in request"); Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client secret not provided in request");
context.challenge(challengeResponse); context.challenge(challengeResponse);
@ -75,30 +115,6 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
context.success(); context.success();
} }
protected String getClientSecret(ClientAuthenticationFlowContext context) {
String clientSecret = null;
String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
if (authorizationHeader != null) {
String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
if (usernameSecret != null) {
clientSecret = usernameSecret[1];
}
}
if (clientSecret == null) {
clientSecret = formData.getFirst("client_secret");
}
return clientSecret;
}
protected void setError(AuthenticationFlowContext context, Response challengeResponse) {
context.getEvent().error(Errors.INVALID_CLIENT_CREDENTIALS);
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
}
@Override @Override
public String getDisplayType() { public String getDisplayType() {
return "Client Id and Secret"; return "Client Id and Secret";

View file

@ -30,7 +30,7 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
protected static Logger logger = Logger.getLogger(JWTClientAuthenticator.class); protected static Logger logger = Logger.getLogger(JWTClientAuthenticator.class);
public static final String PROVIDER_ID = "client-signed-jwt"; public static final String PROVIDER_ID = "client-jwt";
public static final String CERTIFICATE_ATTR = "jwt.credential.certificate"; public static final String CERTIFICATE_ATTR = "jwt.credential.certificate";
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {

View file

@ -1,88 +0,0 @@
package org.keycloak.authentication.authenticators.client;
import java.util.LinkedList;
import java.util.List;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.util.BasicAuthHelper;
/**
* TODO: Should be removed? Or allowed just per public clients?
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ValidateClientId extends AbstractClientAuthenticator {
public static final String PROVIDER_ID = "client-id";
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED
};
@Override
public void authenticateClient(ClientAuthenticationFlowContext context) {
ClientModel client = ClientAuthUtil.getClientFromClientId(context);
if (client == null) {
return;
}
context.setClient(client);
context.success();
}
@Override
public String getDisplayType() {
return "Client ID Validation";
}
@Override
public boolean isConfigurable() {
return false;
}
@Override
public boolean isConfigurablePerClient() {
return false;
}
@Override
public boolean requiresClient() {
return false;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, ClientModel client) {
return true;
}
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public String getHelpText() {
return "Validates the clientId supplied as a 'client_id' form parameter or in 'Authorization: Basic' header";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return new LinkedList<>();
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -1,139 +0,0 @@
package org.keycloak.protocol.oidc;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection;
import org.keycloak.OAuth2Constants;
import org.keycloak.constants.ServiceAccountConstants;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors;
/**
* Endpoint for authenticate clients and retrieve service accounts
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ServiceAccountManager {
protected static final Logger logger = Logger.getLogger(ServiceAccountManager.class);
private TokenManager tokenManager;
private EventBuilder event;
private HttpRequest request;
private MultivaluedMap<String, String> formParams;
private KeycloakSession session;
private UriInfo uriInfo;
private ClientConnection clientConnection;
private ClientModel client;
private UserModel clientUser;
public ServiceAccountManager(TokenManager tokenManager, EventBuilder event, HttpRequest request,
MultivaluedMap<String, String> formParams, KeycloakSession session, ClientModel client) {
this.tokenManager = tokenManager;
this.event = event;
this.request = request;
this.formParams = formParams;
this.session = session;
this.client = client;
this.uriInfo = session.getContext().getUri();
this.clientConnection = session.getContext().getConnection();
}
public Response buildClientCredentialsGrant() {
checkClient();
return finishClientAuthorization();
}
protected void checkClient() {
if (client.isBearerOnly()) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException("unauthorized_client", "Bearer-only client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
}
if (client.isPublicClient()) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException("unauthorized_client", "Public client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
}
if (!client.isServiceAccountsEnabled()) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException("unauthorized_client", "Client not enabled to retrieve service account", Response.Status.UNAUTHORIZED);
}
}
protected Response finishClientAuthorization() {
RealmModel realm = client.getRealm();
event.detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH);
clientUser = session.users().getUserByServiceAccountClient(client);
if (clientUser == null || client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) {
// May need to handle bootstrap here as well
logger.infof("Service account user for client '%s' not found or default protocol mapper for service account not found. Creating now", client.getClientId());
new ClientManager(new RealmManager(session)).enableServiceAccount(client);
clientUser = session.users().getUserByServiceAccountClient(client);
}
String clientUsername = clientUser.getUsername();
event.detail(Details.USERNAME, clientUsername);
event.user(clientUser);
if (!clientUser.isEnabled()) {
event.error(Errors.USER_DISABLED);
throw new ErrorResponseException("invalid_request", "User '" + clientUsername + "' disabled", Response.Status.UNAUTHORIZED);
}
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
UserSessionProvider sessions = session.sessions();
// TODO: Once more requirements are added, clientSession will be likely created earlier by authentication mechanism
ClientSessionModel clientSession = sessions.createClientSession(realm, client);
clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
// TODO: Should rather obtain authMethod from client session?
UserSessionModel userSession = sessions.createUserSession(realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
event.session(userSession);
TokenManager.attachClientSession(userSession, clientSession);
// Notes about client details
userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId());
userSession.setNote(ServiceAccountConstants.CLIENT_HOST, clientConnection.getRemoteHost());
userSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, clientConnection.getRemoteAddr());
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession)
.generateAccessToken(session, scope, client, clientUser, userSession, clientSession)
.generateRefreshToken()
.generateIDToken()
.build();
event.success();
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
}

View file

@ -190,7 +190,7 @@ public class LogoutEndpoint {
} }
private ClientModel authorizeClient() { private ClientModel authorizeClient() {
ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm); ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm).getClient();
if (client.isBearerOnly()) { if (client.isBearerOnly()) {
throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST); throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST);

View file

@ -7,6 +7,7 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.constants.AdapterConstants; import org.keycloak.constants.AdapterConstants;
import org.keycloak.constants.ServiceAccountConstants;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
@ -19,17 +20,16 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.ServiceAccountManager;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors; import org.keycloak.services.resources.Cors;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
@ -52,6 +52,7 @@ public class TokenEndpoint {
private static final Logger logger = Logger.getLogger(TokenEndpoint.class); private static final Logger logger = Logger.getLogger(TokenEndpoint.class);
private MultivaluedMap<String, String> formParams; private MultivaluedMap<String, String> formParams;
private ClientModel client; private ClientModel client;
private Map<String, String> clientAuthAttributes;
private enum Action { private enum Action {
AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS
@ -144,7 +145,9 @@ public class TokenEndpoint {
} }
private void checkClient() { private void checkClient() {
client = AuthorizeClientUtil.authorizeClient(session, event, realm); AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event, realm);
client = clientAuth.getClient();
clientAuthAttributes = clientAuth.getClientAuthAttributes();
if (client.isBearerOnly()) { if (client.isBearerOnly()) {
throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST); throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST);
@ -237,6 +240,7 @@ public class TokenEndpoint {
} }
updateClientSession(clientSession); updateClientSession(clientSession);
updateUserSessionFromClientAuth(userSession);
AccessToken token = tokenManager.createClientAccessToken(session, accessCode.getRequestedRoles(), realm, client, user, userSession, clientSession); AccessToken token = tokenManager.createClientAccessToken(session, accessCode.getRequestedRoles(), realm, client, user, userSession, clientSession);
@ -262,6 +266,7 @@ public class TokenEndpoint {
UserSessionModel userSession = session.sessions().getUserSession(realm, res.getSessionState()); UserSessionModel userSession = session.sessions().getUserSession(realm, res.getSessionState());
updateClientSessions(userSession.getClientSessions()); updateClientSessions(userSession.getClientSessions());
updateUserSessionFromClientAuth(userSession);
} catch (OAuthErrorException e) { } catch (OAuthErrorException e) {
event.error(Errors.INVALID_TOKEN); event.error(Errors.INVALID_TOKEN);
@ -312,6 +317,12 @@ public class TokenEndpoint {
} }
} }
private void updateUserSessionFromClientAuth(UserSessionModel userSession) {
for (Map.Entry<String, String> attr : clientAuthAttributes.entrySet()) {
userSession.setNote(attr.getKey(), attr.getValue());
}
}
public Response buildResourceOwnerPasswordCredentialsGrant() { public Response buildResourceOwnerPasswordCredentialsGrant() {
event.detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token"); event.detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token");
@ -349,6 +360,7 @@ public class TokenEndpoint {
} }
processor.attachSession(); processor.attachSession();
UserSessionModel userSession = processor.getUserSession(); UserSessionModel userSession = processor.getUserSession();
updateUserSessionFromClientAuth(userSession);
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession) AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession)
.generateAccessToken(session, scope, client, user, userSession, clientSession) .generateAccessToken(session, scope, client, user, userSession, clientSession)
@ -363,8 +375,68 @@ public class TokenEndpoint {
} }
public Response buildClientCredentialsGrant() { public Response buildClientCredentialsGrant() {
ServiceAccountManager serviceAccountManager = new ServiceAccountManager(tokenManager, event, request, formParams, session, client); if (client.isBearerOnly()) {
return serviceAccountManager.buildClientCredentialsGrant(); event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException("unauthorized_client", "Bearer-only client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
}
if (client.isPublicClient()) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException("unauthorized_client", "Public client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
}
if (!client.isServiceAccountsEnabled()) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException("unauthorized_client", "Client not enabled to retrieve service account", Response.Status.UNAUTHORIZED);
}
event.detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH);
UserModel clientUser = session.users().getUserByServiceAccountClient(client);
if (clientUser == null || client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) {
// May need to handle bootstrap here as well
logger.infof("Service account user for client '%s' not found or default protocol mapper for service account not found. Creating now", client.getClientId());
new ClientManager(new RealmManager(session)).enableServiceAccount(client);
clientUser = session.users().getUserByServiceAccountClient(client);
}
String clientUsername = clientUser.getUsername();
event.detail(Details.USERNAME, clientUsername);
event.user(clientUser);
if (!clientUser.isEnabled()) {
event.error(Errors.USER_DISABLED);
throw new ErrorResponseException("invalid_request", "User '" + clientUsername + "' disabled", Response.Status.UNAUTHORIZED);
}
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
UserSessionProvider sessions = session.sessions();
ClientSessionModel clientSession = sessions.createClientSession(realm, client);
clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
UserSessionModel userSession = sessions.createUserSession(realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
event.session(userSession);
TokenManager.attachClientSession(userSession, clientSession);
// Notes about client details
userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId());
userSession.setNote(ServiceAccountConstants.CLIENT_HOST, clientConnection.getRemoteHost());
userSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, clientConnection.getRemoteAddr());
updateUserSessionFromClientAuth(userSession);
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession)
.generateAccessToken(session, scope, client, clientUser, userSession, clientSession)
.generateRefreshToken()
.generateIDToken()
.build();
event.success();
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
} }
} }

View file

@ -1,5 +1,7 @@
package org.keycloak.protocol.oidc.utils; package org.keycloak.protocol.oidc.utils;
import java.util.Map;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
@ -17,7 +19,7 @@ import javax.ws.rs.core.Response;
*/ */
public class AuthorizeClientUtil { public class AuthorizeClientUtil {
public static ClientModel authorizeClient(KeycloakSession session, EventBuilder event, RealmModel realm) { public static ClientAuthResult authorizeClient(KeycloakSession session, EventBuilder event, RealmModel realm) {
AuthenticationFlowModel clientAuthFlow = realm.getClientAuthenticationFlow(); AuthenticationFlowModel clientAuthFlow = realm.getClientAuthenticationFlow();
String flowId = clientAuthFlow.getId(); String flowId = clientAuthFlow.getId();
@ -40,7 +42,26 @@ public class AuthorizeClientUtil {
throw new ErrorResponseException("invalid_client", "Client authentication was successful, but client is null", Response.Status.BAD_REQUEST); throw new ErrorResponseException("invalid_client", "Client authentication was successful, but client is null", Response.Status.BAD_REQUEST);
} }
return client; return new ClientAuthResult(client, processor.getClientAuthAttributes());
}
public static class ClientAuthResult {
private final ClientModel client;
private final Map<String, String> clientAuthAttributes;
private ClientAuthResult(ClientModel client, Map<String, String> clientAuthAttributes) {
this.client = client;
this.clientAuthAttributes = clientAuthAttributes;
}
public ClientModel getClient() {
return client;
}
public Map<String, String> getClientAuthAttributes() {
return clientAuthAttributes;
}
} }
} }

View file

@ -153,7 +153,7 @@ public class ClientsManagementService {
} }
protected ClientModel authorizeClient() { protected ClientModel authorizeClient() {
ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm); ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm).getClient();
if (client.isPublicClient()) { if (client.isPublicClient()) {
Map<String, String> error = new HashMap<String, String>(); Map<String, String> error = new HashMap<String, String>();

View file

@ -1,5 +1,6 @@
package org.keycloak.testsuite.oauth; package org.keycloak.testsuite.oauth;
import java.security.PrivateKey;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
@ -17,7 +18,7 @@ import org.junit.ClassRule;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.ClientAuthAdapterUtils; import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.constants.ServiceAccountConstants; import org.keycloak.constants.ServiceAccountConstants;
import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.constants.ServiceUrlConstants;
@ -238,8 +239,8 @@ public class ClientAuthSignedJWTTest {
@Test @Test
public void testAssertionMissingIssuer() throws Exception { public void testAssertionMissingIssuer() throws Exception {
String invalidJwt = ClientAuthAdapterUtils.createSignedJWT(null, getRealmInfoUrl(), String invalidJwt = getClientSignedJWT(
"classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10); "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, null);
List<NameValuePair> parameters = new LinkedList<NameValuePair>(); List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
@ -254,8 +255,8 @@ public class ClientAuthSignedJWTTest {
@Test @Test
public void testAssertionUnknownClient() throws Exception { public void testAssertionUnknownClient() throws Exception {
String invalidJwt = ClientAuthAdapterUtils.createSignedJWT("unknown-client", getRealmInfoUrl(), String invalidJwt = getClientSignedJWT(
"classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10); "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, "unknown-client");
List<NameValuePair> parameters = new LinkedList<NameValuePair>(); List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
@ -339,24 +340,8 @@ public class ClientAuthSignedJWTTest {
@Test @Test
public void testAssertionInvalidSignature() throws Exception { public void testAssertionInvalidSignature() throws Exception {
// JWT for client1, but signed by privateKey of client2 // JWT for client1, but signed by privateKey of client2
String invalidJwt = ClientAuthAdapterUtils.createSignedJWT("client1", getRealmInfoUrl(), String invalidJwt = getClientSignedJWT(
"classpath:client-auth-test/keystore-client2.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10); "classpath:client-auth-test/keystore-client2.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, "client1");
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt));
HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
assertError(response, "client1", "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS);
}
@Test
public void testAssertionInvalidAudience() throws Exception {
String invalidJwt = ClientAuthAdapterUtils.createSignedJWT("client1", "invalid-audience",
"classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
List<NameValuePair> parameters = new LinkedList<NameValuePair>(); List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
@ -491,17 +476,20 @@ public class ClientAuthSignedJWTTest {
private String getClient1SignedJWT() { private String getClient1SignedJWT() {
return ClientAuthAdapterUtils.createSignedJWT("client1", getRealmInfoUrl(), return getClientSignedJWT("classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, "client1");
"classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
} }
private String getClient2SignedJWT() { private String getClient2SignedJWT() {
// keystore-client2.p12 doesn't work on Sun JDK due to restrictions on key length return getClientSignedJWT("classpath:client-auth-test/keystore-client2.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, "client2");
// String keystoreFile = "classpath:client-auth-test/keystore-client2.p12"; }
String keystoreFile = "classpath:client-auth-test/keystore-client2.jks"; private String getClientSignedJWT(String keystoreFile, String storePassword, String keyPassword, String keyAlias, KeystoreUtil.KeystoreFormat format, String clientId) {
return ClientAuthAdapterUtils.createSignedJWT("client2", getRealmInfoUrl(), PrivateKey privateKey = KeystoreUtil.loadPrivateKeyFromKeystore(keystoreFile, storePassword, keyPassword, keyAlias, format);
keystoreFile, "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider();
jwtProvider.setPrivateKey(privateKey);
jwtProvider.setTokenTimeout(10);
return jwtProvider.createSignedRequestToken(clientId, getRealmInfoUrl());
} }
private String getRealmInfoUrl() { private String getRealmInfoUrl() {

View file

@ -126,7 +126,9 @@
"redirectUris": [ "redirectUris": [
"http://localhost:8081/secure-portal/*" "http://localhost:8081/secure-portal/*"
], ],
"secret": "password" "attributes": {
"jwt.credential.certificate": "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ=="
}
}, },
{ {
"name": "session-portal", "name": "session-portal",

View file

@ -5,6 +5,13 @@
"auth-server-url" : "http://localhost:8081/auth", "auth-server-url" : "http://localhost:8081/auth",
"ssl-required" : "external", "ssl-required" : "external",
"credentials" : { "credentials" : {
"secret": "password" "jwt": {
} "client-keystore-file": "classpath:adapter-test/secure-portal-keystore.jks",
"client-keystore-type": "JKS",
"client-keystore-password": "storepass",
"client-key-password": "keypass",
"client-key-alias": "clientkey",
"token-expiration": 10
}
}
} }

View file

@ -5,6 +5,13 @@
"auth-server-url" : "http://localhost:8080/auth", "auth-server-url" : "http://localhost:8080/auth",
"ssl-required" : "external", "ssl-required" : "external",
"credentials" : { "credentials" : {
"secret": "password" "jwt": {
} "client-keystore-file": "classpath:adapter-test/secure-portal-keystore.jks",
"client-keystore-type": "JKS",
"client-keystore-password": "storepass",
"client-key-password": "keypass",
"client-key-alias": "clientkey",
"token-expiration": 10
}
}
} }