From 18583eb6ed8e81216f76b0f7bbe4c233bccf1ea5 Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 18 Aug 2015 12:26:07 +0200 Subject: [PATCH 1/4] Fix mongo --- .../mongo/DefaultMongoConnectionFactoryProvider.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index 8da15b69c1..423bf3e5e0 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -27,26 +27,23 @@ import java.util.Map; */ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionProviderFactory { - // TODO Make configurable + // TODO Make it dynamic private String[] entities = new String[]{ "org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity", "org.keycloak.models.mongo.keycloak.entities.MongoUserEntity", "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.ClientIdentityProviderMappingEntity", "org.keycloak.models.entities.RequiredCredentialEntity", "org.keycloak.models.entities.CredentialEntity", "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.UserFederationMapperEntity", "org.keycloak.models.entities.ProtocolMapperEntity", "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.AuthenticationFlowEntity", "org.keycloak.models.entities.AuthenticatorConfigEntity", From 87f7ec59099c94fb00621b206a27da990e8774d7 Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 18 Aug 2015 12:44:21 +0200 Subject: [PATCH 2/4] KEYCLOAK-1561 LDAPDn.getParentDn() return value is not a DN --- federation/ldap/pom.xml | 5 +++++ .../federation/ldap/idm/model/LDAPDn.java | 9 +++++++- .../federation/ldap/idm/model/LDAPDnTest.java | 22 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java diff --git a/federation/ldap/pom.xml b/federation/ldap/pom.xml index bc60afed58..0043d9aefe 100755 --- a/federation/ldap/pom.xml +++ b/federation/ldap/pom.xml @@ -52,6 +52,11 @@ jboss-logging provided + + junit + junit + test + diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java index f1cd3414d8..a7cf098702 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java @@ -1,5 +1,6 @@ package org.keycloak.federation.ldap.idm.model; +import java.util.Collection; import java.util.Deque; import java.util.LinkedList; import java.util.regex.Matcher; @@ -26,6 +27,10 @@ public class LDAPDn { @Override public String toString() { + return toString(entries); + } + + private static String toString(Collection entries) { StringBuilder builder = new StringBuilder(); 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" */ public String getParentDn() { - return new LinkedList<>(entries).remove().toString(); + LinkedList parentDnEntries = new LinkedList<>(entries); + parentDnEntries.remove(); + return toString(parentDnEntries); } public void addFirst(String rdnName, String rdnValue) { diff --git a/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java b/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java new file mode 100644 index 0000000000..77bc4ceb60 --- /dev/null +++ b/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java @@ -0,0 +1,22 @@ +package org.keycloak.federation.ldap.idm.model; + +import org.junit.Assert; +import org.junit.Test; + +/** + * @author Marek Posolda + */ +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()); + } +} From d8d6348f67c4f3dc2c8e11b80ef1ad6c96386d3c Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 18 Aug 2015 16:56:41 +0200 Subject: [PATCH 3/4] KEYCLOAK-1295 Adapter support. Fixes --- .../org/keycloak/AbstractOAuthClient.java | 6 +- .../adapters/config/BaseAdapterConfig.java | 6 +- .../example/ProductSAClientSecretServlet.java | 19 +++ .../ProductSAClientSignedJWTServlet.java | 17 +++ .../example/ProductServiceAccountServlet.java | 49 ++++-- .../src/main/resources/keystore-client.jks | Bin 0 -> 2028 bytes ...cloak.json => keycloak-client-secret.json} | 0 .../WEB-INF/keycloak-client-signed-jwt.json | 17 +++ .../src/main/webapp/WEB-INF/page.jsp | 10 +- .../src/main/webapp/WEB-INF/web.xml | 18 ++- .../src/main/webapp/index.html | 10 +- examples/demo-template/testrealm.json | 5 +- .../example/oauth/AppContextListener.java | 1 - .../org/keycloak/example/oauth/Bootstrap.java | 2 - .../theme/base/admin/resources/js/app.js | 6 +- .../admin/resources/js/controllers/clients.js | 14 +- .../admin/resources/js/controllers/realm.js | 14 +- .../authentication-flow-bindings.html | 11 ++ .../client-credentials-jwt-key-export.html | 2 +- .../client-credentials-jwt-key-import.html | 2 +- .../partials/client-credentials-jwt.html | 6 +- .../admin/resources/partials/create-flow.html | 2 +- .../adapters/AdapterDeploymentContext.java | 15 +- .../BasicAuthRequestAuthenticator.java | 9 +- .../adapters/ClientAuthAdapterUtils.java | 50 ------- .../keycloak/adapters/KeycloakDeployment.java | 16 +- .../adapters/KeycloakDeploymentBuilder.java | 4 + .../org/keycloak/adapters/ServerRequest.java | 109 +++----------- .../ClientCredentialsProvider.java | 19 +++ .../ClientCredentialsProviderUtils.java | 85 +++++++++++ .../ClientIdAndSecretCredentialsProvider.java | 49 ++++++ .../JWTClientCredentialsProvider.java | 103 +++++++++++++ .../jaas/DirectAccessGrantsLoginModule.java | 21 +-- ...s.authentication.ClientCredentialsProvider | 2 + .../KeycloakDeploymentBuilderTest.java | 17 +++ .../src/test/resources/keycloak-jwt.json | 13 ++ .../resources/keycloak-no-credentials.json | 8 + .../src/test/resources/keycloak.json | 4 +- .../src/test/resources/keystore.jks | Bin 2249 -> 2051 bytes .../org/keycloak/jaxrs/JaxrsOAuthClient.java | 4 +- integration/servlet-oauth-client/pom.xml | 21 +++ ...KeycloakDeploymentDelegateOAuthClient.java | 83 +++++++++++ .../keycloak/servlet/ServletOAuthClient.java | 22 +-- .../servlet/ServletOAuthClientBuilder.java | 71 ++------- .../ServletOAuthClientBuilderTest.java | 25 ++++ .../src/test/resources/keycloak.json | 28 ++++ .../utils/DefaultAuthenticationFlows.java | 2 +- .../AuthenticationProcessor.java | 11 ++ .../ClientAuthenticationFlow.java | 14 -- .../ClientAuthenticationFlowContext.java | 13 ++ .../authenticators/client/ClientAuthUtil.java | 47 ------ .../ClientIdAndSecretAuthenticator.java | 78 ++++++---- .../client/JWTClientAuthenticator.java | 2 +- .../client/ValidateClientId.java | 88 ----------- .../protocol/oidc/ServiceAccountManager.java | 139 ------------------ .../oidc/endpoints/LogoutEndpoint.java | 2 +- .../oidc/endpoints/TokenEndpoint.java | 84 ++++++++++- .../oidc/utils/AuthorizeClientUtil.java | 25 +++- .../resources/ClientsManagementService.java | 2 +- .../oauth/ClientAuthSignedJWTTest.java | 48 +++--- .../resources/adapter-test/demorealm.json | 4 +- .../adapter-test/secure-portal-keycloak.json | 11 +- .../adapter-test/secure-portal-keystore.jks | Bin 0 -> 2028 bytes .../secure-portal/WEB-INF/keycloak.json | 11 +- 64 files changed, 915 insertions(+), 661 deletions(-) create mode 100644 examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSecretServlet.java create mode 100644 examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java create mode 100644 examples/demo-template/service-account/src/main/resources/keystore-client.jks rename examples/demo-template/service-account/src/main/webapp/WEB-INF/{keycloak.json => keycloak-client-secret.json} (100%) create mode 100644 examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-signed-jwt.json delete mode 100644 integration/adapter-core/src/main/java/org/keycloak/adapters/ClientAuthAdapterUtils.java create mode 100644 integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java create mode 100644 integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java create mode 100644 integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientIdAndSecretCredentialsProvider.java create mode 100644 integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java create mode 100644 integration/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider create mode 100644 integration/adapter-core/src/test/resources/keycloak-jwt.json create mode 100644 integration/adapter-core/src/test/resources/keycloak-no-credentials.json create mode 100644 integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java create mode 100644 integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java create mode 100644 integration/servlet-oauth-client/src/test/resources/keycloak.json delete mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/client/ValidateClientId.java delete mode 100644 services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java create mode 100644 testsuite/integration/src/test/resources/adapter-test/secure-portal-keystore.jks diff --git a/core/src/main/java/org/keycloak/AbstractOAuthClient.java b/core/src/main/java/org/keycloak/AbstractOAuthClient.java index 2a959052a8..3e32bbded2 100755 --- a/core/src/main/java/org/keycloak/AbstractOAuthClient.java +++ b/core/src/main/java/org/keycloak/AbstractOAuthClient.java @@ -16,7 +16,7 @@ public class AbstractOAuthClient { private final AtomicLong counter = new AtomicLong(); protected String clientId; - protected Map credentials; + protected Map credentials; protected String authUrl; protected String tokenUrl; protected RelativeUrlsUsed relativeUrlsUsed; @@ -37,11 +37,11 @@ public class AbstractOAuthClient { this.clientId = clientId; } - public Map getCredentials() { + public Map getCredentials() { return credentials; } - public void setCredentials(Map credentials) { + public void setCredentials(Map credentials) { this.credentials = credentials; } diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java index 608cbaa3f9..a71dfe7dbd 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java @@ -39,7 +39,7 @@ public class BaseAdapterConfig extends BaseRealmConfig { @JsonProperty("public-client") protected boolean publicClient; @JsonProperty("credentials") - protected Map credentials = new HashMap(); + protected Map credentials = new HashMap<>(); public boolean isUseResourceRoleMappings() { @@ -114,11 +114,11 @@ public class BaseAdapterConfig extends BaseRealmConfig { this.enableBasicAuth = enableBasicAuth; } - public Map getCredentials() { + public Map getCredentials() { return credentials; } - public void setCredentials(Map credentials) { + public void setCredentials(Map credentials) { this.credentials = credentials; } diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSecretServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSecretServlet.java new file mode 100644 index 0000000000..43c70f8127 --- /dev/null +++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSecretServlet.java @@ -0,0 +1,19 @@ +package org.keycloak.example; + +/** + * Client authentication with traditional OAuth2 client_id + client_secret + * + * @author Marek Posolda + */ +public class ProductSAClientSecretServlet extends ProductServiceAccountServlet { + + @Override + protected String getAdapterConfigLocation() { + return "WEB-INF/keycloak-client-secret.json"; + } + + @Override + protected String getClientAuthenticationMethod() { + return "secret"; + } +} diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java new file mode 100644 index 0000000000..2a6fe337b9 --- /dev/null +++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java @@ -0,0 +1,17 @@ +package org.keycloak.example; + +/** + * @author Marek Posolda + */ +public class ProductSAClientSignedJWTServlet extends ProductServiceAccountServlet { + + @Override + protected String getAdapterConfigLocation() { + return "WEB-INF/keycloak-client-signed-jwt.json"; + } + + @Override + protected String getClientAuthenticationMethod() { + return "jwt"; + } +} diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java index d03654d542..7ce0701c52 100644 --- a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java +++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java @@ -4,7 +4,9 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -26,30 +28,46 @@ import org.keycloak.VerificationException; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; 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.AccessTokenResponse; -import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.JsonSerialization; /** * @author Marek Posolda */ -public class ProductServiceAccountServlet extends HttpServlet { +public abstract class ProductServiceAccountServlet extends HttpServlet { public static final String ERROR = "error"; public static final String TOKEN = "token"; public static final String TOKEN_PARSED = "idTokenParsed"; public static final String REFRESH_TOKEN = "refreshToken"; 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 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); - 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); } @@ -60,6 +78,8 @@ public class ProductServiceAccountServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + req.setAttribute(CLIENT_AUTH_METHOD, getClientAuthenticationMethod()); + String reqUri = req.getRequestURI(); if (reqUri.endsWith("/login")) { serviceAccountLogin(req); @@ -81,16 +101,21 @@ public class ProductServiceAccountServlet extends HttpServlet { KeycloakDeployment deployment = getKeycloakDeployment(); HttpClient client = getHttpClient(); - String clientId = deployment.getResourceName(); - String clientSecret = deployment.getResourceCredentials().get("secret"); - try { HttpPost post = new HttpPost(deployment.getTokenUrl()); List formparams = new ArrayList(); formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); - String authHeader = BasicAuthHelper.createHeader(clientId, clientSecret); - post.addHeader("Authorization", authHeader); + // Add client credentials according to the method configured in keycloak.json file + Map reqHeaders = new HashMap<>(); + Map reqParams = new HashMap<>(); + ClientCredentialsProviderUtils.setClientCredentials(deployment, reqHeaders, reqParams); + for (Map.Entry header : reqHeaders.entrySet()) { + post.setHeader(header.getKey(), header.getValue()); + } + for (Map.Entry param : reqParams.entrySet()) { + formparams.add(new BasicNameValuePair(param.getKey(), param.getValue())); + } UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); post.setEntity(form); @@ -217,7 +242,7 @@ public class ProductServiceAccountServlet extends HttpServlet { } private KeycloakDeployment getKeycloakDeployment() { - return (KeycloakDeployment) getServletContext().getAttribute(KeycloakDeployment.class.getName()); + return (KeycloakDeployment) getServletContext().getAttribute("deployment-" + getClientAuthenticationMethod()); } private HttpClient getHttpClient() { diff --git a/examples/demo-template/service-account/src/main/resources/keystore-client.jks b/examples/demo-template/service-account/src/main/resources/keystore-client.jks new file mode 100644 index 0000000000000000000000000000000000000000..9b2a4d6753db04b0eed4be54134800c7be59d058 GIT binary patch literal 2028 zcmV?yV%b+X<$6B72zV^b(@+A>)zJ^ zKO=-5?_`f~=}x8FF`K{C1KUJvm$RS;Cz9anI{KZ2sI2GrCPz!i(1OiP+E($ZoJ30G z2Xkyy;mj|6rli?%&Fzu%*7)xb z`*l9ciq7Rg$Eh}3msS7$K;nS%M<8zsolEFYQM!Dmcjfa)<5lAu6wD>HGtzB)hvDF1 zgD;%e=tX;qi&Eqq9g?Z$*x$W(V`6+Sb~t3y?dSP)Q6}z$(5fO|Et*o$7uz6&W(}b; z^vhaxGkbAi1KyA!j?nqI`ZhlMkrQEkMwG!wA7rmoXG8dCJ1(XW46j!dWs~5c<*hmj zeu@%BWGSHa6@(HR7e z!ZMrO9D8WZEnfcLL`v(GFPow2YV0+wky-1nxj<8zd5OZ(q9O&qQukKQI1EyXaJX$M zY5oQ)w2HfBwSvgxQ}q!ILoymqP{K`Nk;NL-7IX z>g3J~QUbLckPyrB4(axOiMeUm?wJc6VyFDMUjeCi~} zuS#2( z+FG#eO?QP^ph&@O5`+-v^BoCR{L|46F|ph9IydbUc6RC^tJr z5(g9v6dT2$ZRBk;bwV@OWJqGzCrO)#%HDw^M#R z`BwP67EA1rN5k2D=A~Ppdn5lPDNp)m3ot&#H#v|objX_46x>>D*KGpH3pO2XKWucj z3v#owA+WiJumt@W&`Q(_DoqR<%>&<_&^_=f`FC3=5#YlcVHCa9%%7K$xV->v-fJ{3 zq`UI?M)IPp6aSaN0JbhI#C=4PSfrw4cq=tesC$HC0XsGok*cz5&8t2;)}LK|q>0f& zGXs;Nxn?_-f?pBayi7;vVws+$dQ!bA-`?cIX>gF16Sf+4tvGre$Z}Yd*|98cWf}r3 zIyY(g-S%dg{5y|OGO^=r`5{KQhJ%Q1uC<^L<*5ge{u6cgP-rkZ+^h_xb{`%K{-n4( zNbN5JS7$snRqHc{pUuWlgUv|_Yc}f=$a!I?7l8ax6vkL;6S|}i!huDN@o5IyJ|yz000311z0XMFgXAK0--R10-Z2|0fhnv z0Z%-z+D|YI1_>&LNQUU9~12ZQQ1P~CTL5yAdfpQx5}T5?dMrz8BA47go#;b})p3^cF8^t*nADo+%< zvks^&J^<*pQ!$8g77XTlgWupigI8?ZHY0utXRS4+<&G|s#$2#J%53!C|GRr}1>&VvtilN0Vl8AVZ}Ue3p@UQ~SXKY3Yu$I} z%f($0=~|Hv8T&!%c5@sI#Jd6m0RRCo4F(A+hDe6@4FLfQ1potr0RaFgPL2bPtbc7A z&%)V0h>fzOa0Ni*Xu+3$dK>P~;U$s?z%endW zF)nweIgS_eCMHayWjsaqBQ?6s(b$=nc){qj2*5DV}ox literal 0 HcmV?d00001 diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-secret.json similarity index 100% rename from examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak.json rename to examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-secret.json diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-signed-jwt.json b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-signed-jwt.json new file mode 100644 index 0000000000..6c07d1eb90 --- /dev/null +++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-signed-jwt.json @@ -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 + } + } +} \ No newline at end of file diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp b/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp index e151f96b49..8296ddb43b 100644 --- a/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp +++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp @@ -13,10 +13,16 @@ AccessToken token = (AccessToken) request.getSession().getAttribute(ProductServiceAccountServlet.TOKEN_PARSED); String products = (String) request.getAttribute(ProductServiceAccountServlet.PRODUCTS); 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); %>

Service account portal

-

Login | Refresh token | Logout

+

Client authentication method: <%= clientAuthMethod %>

+

Login | Refresh token | Logout


<% if (appError != null) { %> diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml b/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml index 5dc7103ac0..024a52272c 100644 --- a/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml +++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml @@ -7,13 +7,23 @@ service-account-portal - ServiceAccountExample - org.keycloak.example.ProductServiceAccountServlet + ProductSAClientSecretServlet + org.keycloak.example.ProductSAClientSecretServlet + + + + ProductSAClientSignedJWTServlet + org.keycloak.example.ProductSAClientSignedJWTServlet - ServiceAccountExample - /app/* + ProductSAClientSecretServlet + /app-secret/* + + + + ProductSAClientSignedJWTServlet + /app-jwt/* \ No newline at end of file diff --git a/examples/demo-template/service-account/src/main/webapp/index.html b/examples/demo-template/service-account/src/main/webapp/index.html index e2820d1744..8c79bc6273 100644 --- a/examples/demo-template/service-account/src/main/webapp/index.html +++ b/examples/demo-template/service-account/src/main/webapp/index.html @@ -1,5 +1,9 @@ - - - + Service account example + + + \ No newline at end of file diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json index 556708efc3..8d3e597258 100755 --- a/examples/demo-template/testrealm.json +++ b/examples/demo-template/testrealm.json @@ -173,7 +173,10 @@ "clientId": "product-sa-client", "enabled": true, "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": { diff --git a/examples/demo-template/third-party-cdi/src/main/java/org/keycloak/example/oauth/AppContextListener.java b/examples/demo-template/third-party-cdi/src/main/java/org/keycloak/example/oauth/AppContextListener.java index bd6a395242..1fe4a2ff71 100755 --- a/examples/demo-template/third-party-cdi/src/main/java/org/keycloak/example/oauth/AppContextListener.java +++ b/examples/demo-template/third-party-cdi/src/main/java/org/keycloak/example/oauth/AppContextListener.java @@ -41,7 +41,6 @@ public class AppContextListener implements ServletContextListener { } ServletOAuthClientBuilder.build(is, oauthClient); logger.info("OAuth client configured and started"); - oauthClient.start(); } @Override diff --git a/examples/demo-template/third-party/src/main/java/org/keycloak/example/oauth/Bootstrap.java b/examples/demo-template/third-party/src/main/java/org/keycloak/example/oauth/Bootstrap.java index a3972b3c21..cb6ac99537 100755 --- a/examples/demo-template/third-party/src/main/java/org/keycloak/example/oauth/Bootstrap.java +++ b/examples/demo-template/third-party/src/main/java/org/keycloak/example/oauth/Bootstrap.java @@ -27,8 +27,6 @@ public class Bootstrap implements ServletContextListener { ServletContext context = sce.getServletContext(); configureClient(context); - - client.start(); context.setAttribute(ServletOAuthClient.class.getName(), client); } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index d0fc4024f8..dfbd4dadac 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -654,7 +654,7 @@ module.config([ '$routeProvider', function($routeProvider) { }, 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', resolve : { realm : function(RealmLoader) { @@ -666,7 +666,7 @@ module.config([ '$routeProvider', function($routeProvider) { }, 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', resolve : { realm : function(RealmLoader) { @@ -681,7 +681,7 @@ module.config([ '$routeProvider', function($routeProvider) { }, 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', resolve : { realm : function(RealmLoader) { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index b50c4a02d2..c6924c9c6f 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -81,11 +81,11 @@ module.controller('ClientSignedJWTCtrl', function($scope, $location, realm, clie ); $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() { - $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() { @@ -263,7 +263,7 @@ module.controller('ClientCertificateImportCtrl', function($scope, $location, $ht var redirectLocation = "/realms/" + realm.realm + "/clients/" + client.id + "/saml/keys"; } else if (callingContext == 'jwt-credentials') { 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 = []; @@ -371,6 +371,12 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht }); var ext = ".jks"; 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); }).error(function(data) { var errorMsg = 'Error downloading'; @@ -390,7 +396,7 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht }); $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"); } }); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index 00da83c290..a33b84fd0a 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -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) { - $scope.flows = flows; + $scope.flows = []; + $scope.clientFlows = []; + for (var i=0 ; iSelect the flow you want to use when the user has forgotten their credentials. +
+ +
+
+ +
+
+ Select the flow you want to use for authentication of clients. +
+
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html index b933e9aa47..b00a22797c 100644 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html @@ -3,7 +3,7 @@ diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html index 82b255d59b..f11a1eab75 100644 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html @@ -3,7 +3,7 @@ diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html index 5f580ab2bc..5290b1871b 100644 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html @@ -9,7 +9,7 @@
- Client Certificate Client Certificate for validate JWT issued by client and signed by Client private key. + Client Certificate Client Certificate for validate JWT issued by client and signed by Client private key from your keystore.
@@ -19,11 +19,11 @@
- +
- +
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html index 564261a977..ccc9fd7224 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html @@ -29,7 +29,7 @@
- What kind of top level flow is it? Type 'client' is used for authentication of clients (applications) when generic is for everything else + 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
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java index b9b495b152..76aaec38ce 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java @@ -5,6 +5,7 @@ import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.jboss.logging.Logger; +import org.keycloak.adapters.authentication.ClientCredentialsProvider; import org.keycloak.enums.RelativeUrlsUsed; import org.keycloak.enums.SslRequired; import org.keycloak.enums.TokenStore; @@ -253,15 +254,25 @@ public class AdapterDeploymentContext { } @Override - public Map getResourceCredentials() { + public Map getResourceCredentials() { return delegate.getResourceCredentials(); } @Override - public void setResourceCredentials(Map resourceCredentials) { + public void setResourceCredentials(Map resourceCredentials) { delegate.setResourceCredentials(resourceCredentials); } + @Override + public void setClientAuthenticator(ClientCredentialsProvider clientAuthenticator) { + delegate.setClientAuthenticator(clientAuthenticator); + } + + @Override + public ClientCredentialsProvider getClientAuthenticator() { + return delegate.getClientAuthenticator(); + } + @Override public HttpClient getClient() { return delegate.getClient(); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java index ae3888cbcc..5ce1b6ed2a 100644 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java @@ -9,6 +9,7 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.message.BasicNameValuePair; import org.jboss.logging.Logger; import org.keycloak.OAuth2Constants; +import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils; import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.util.BasicAuthHelper; @@ -76,13 +77,7 @@ public class BasicAuthRequestAuthenticator extends BearerTokenRequestAuthenticat formparams.add(new BasicNameValuePair("username", username)); formparams.add(new BasicNameValuePair("password", password)); - if (deployment.isPublicClient()) { - formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, deployment.getResourceName())); - } else { - String authorization = BasicAuthHelper.createHeader(deployment.getResourceName(), - deployment.getResourceCredentials().get("secret")); - post.setHeader("Authorization", authorization); - } + ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); post.setEntity(form); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/ClientAuthAdapterUtils.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/ClientAuthAdapterUtils.java deleted file mode 100644 index 42954e3cca..0000000000 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/ClientAuthAdapterUtils.java +++ /dev/null @@ -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 Marek Posolda - */ -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; - } - -} diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java index 01960a4e6a..f927382553 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java @@ -2,6 +2,7 @@ package org.keycloak.adapters; import org.apache.http.client.HttpClient; import org.jboss.logging.Logger; +import org.keycloak.adapters.authentication.ClientCredentialsProvider; import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.enums.RelativeUrlsUsed; import org.keycloak.enums.SslRequired; @@ -39,7 +40,8 @@ public class KeycloakDeployment { protected boolean bearerOnly; protected boolean enableBasicAuth; protected boolean publicClient; - protected Map resourceCredentials = new HashMap(); + protected Map resourceCredentials = new HashMap<>(); + protected ClientCredentialsProvider clientAuthenticator; protected HttpClient client; protected String scope; @@ -216,14 +218,22 @@ public class KeycloakDeployment { this.publicClient = publicClient; } - public Map getResourceCredentials() { + public Map getResourceCredentials() { return resourceCredentials; } - public void setResourceCredentials(Map resourceCredentials) { + public void setResourceCredentials(Map resourceCredentials) { this.resourceCredentials = resourceCredentials; } + public ClientCredentialsProvider getClientAuthenticator() { + return clientAuthenticator; + } + + public void setClientAuthenticator(ClientCredentialsProvider clientAuthenticator) { + this.clientAuthenticator = clientAuthenticator; + } + public HttpClient getClient() { return client; } diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java index 5ddc54b142..c4302583b3 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java @@ -3,6 +3,7 @@ package org.keycloak.adapters; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.jboss.logging.Logger; +import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils; import org.keycloak.enums.SslRequired; import org.keycloak.enums.TokenStore; import org.keycloak.representations.adapters.config.AdapterConfig; @@ -55,7 +56,10 @@ public class KeycloakDeploymentBuilder { deployment.setTokenStore(TokenStore.SESSION); } if (adapterConfig.getPrincipalAttribute() != null) deployment.setPrincipalAttribute(adapterConfig.getPrincipalAttribute()); + deployment.setResourceCredentials(adapterConfig.getCredentials()); + deployment.setClientAuthenticator(ClientCredentialsProviderUtils.bootstrapClientAuthenticator(deployment)); + deployment.setPublicClient(adapterConfig.isPublicClient()); deployment.setUseResourceRoleMappings(adapterConfig.isUseResourceRoleMappings()); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java index 0bf2cf4036..bbfd869c59 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java @@ -8,10 +8,9 @@ import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.message.BasicNameValuePair; import org.keycloak.OAuth2Constants; +import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils; import org.keycloak.constants.AdapterConstants; 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.JsonSerialization; import org.keycloak.util.KeycloakUriBuilder; @@ -23,7 +22,6 @@ import java.io.InputStream; import java.net.URI; import java.util.ArrayList; import java.util.List; -import java.util.Map; /** * @author Bill Burke @@ -50,30 +48,17 @@ public class ServerRequest { } public static void invokeLogout(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure { - String client_id = deployment.getResourceName(); - Map credentials = deployment.getResourceCredentials(); HttpClient client = deployment.getClient(); URI uri = deployment.getLogoutUrl().clone().build(); - List formparams = new ArrayList(); - for (Map.Entry entry : credentials.entrySet()) { - formparams.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); - } + List formparams = new ArrayList<>(); + formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken)); - HttpResponse response = null; HttpPost post = new HttpPost(uri); - if (!deployment.isPublicClient()) { - 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)); - } + ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); post.setEntity(form); - response = client.execute(post); + HttpResponse response = client.execute(post); int status = response.getStatusLine().getStatusCode(); HttpEntity entity = response.getEntity(); if (status != 204) { @@ -86,17 +71,8 @@ public class ServerRequest { if (is != null) is.close(); } - public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri, String sessionId) throws HttpFailure, IOException { - String tokenUrl = deployment.getTokenUrl(); - String client_id = deployment.getResourceName(); - Map 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 credentials, String sessionId) throws IOException, HttpFailure { - List formparams = new ArrayList(); + public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri, String sessionId) throws IOException, HttpFailure { + List formparams = new ArrayList<>(); redirectUri = stripOauthParametersFromRedirect(redirectUri); formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "authorization_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_HOST, HostUtils.getHostName())); } - HttpResponse response = null; - HttpPost post = new HttpPost(tokenUrl); - if (!publicClient) { - 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)); - } + + HttpPost post = new HttpPost(deployment.getTokenUrl()); + ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); post.setEntity(form); - response = client.execute(post); + HttpResponse response = deployment.getClient().execute(post); int status = response.getStatusLine().getStatusCode(); HttpEntity entity = response.getEntity(); if (status != 200) { @@ -152,36 +120,16 @@ public class ServerRequest { } public static AccessTokenResponse invokeRefresh(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure { - String tokenUrl = deployment.getTokenUrl(); - String client_id = deployment.getResourceName(); - Map 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 credentials) throws IOException, HttpFailure { List formparams = new ArrayList(); - for (Map.Entry 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.REFRESH_TOKEN, refreshToken)); - HttpResponse response = null; - HttpPost post = new HttpPost(tokenUrl); - if (!publicClient) { - 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)); - } + + HttpPost post = new HttpPost(deployment.getTokenUrl()); + ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); post.setEntity(form); - response = client.execute(post); + HttpResponse response = deployment.getClient().execute(post); int status = response.getStatusLine().getStatusCode(); HttpEntity entity = response.getEntity(); if (status != 200) { @@ -215,43 +163,28 @@ public class ServerRequest { public static void invokeRegisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException { String registerNodeUrl = deployment.getRegisterNodeUrl(); - String client_id = deployment.getResourceName(); - Map credentials = deployment.getResourceCredentials(); - HttpClient client = deployment.getClient(); - - invokeClientManagementRequest(client, host, registerNodeUrl, client_id, credentials); + invokeClientManagementRequest(deployment, host, registerNodeUrl); } public static void invokeUnregisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException { String unregisterNodeUrl = deployment.getUnregisterNodeUrl(); - String client_id = deployment.getResourceName(); - Map credentials = deployment.getResourceCredentials(); - HttpClient client = deployment.getClient(); - - invokeClientManagementRequest(client, host, unregisterNodeUrl, client_id, credentials); + invokeClientManagementRequest(deployment, host, unregisterNodeUrl); } - public static void invokeClientManagementRequest(HttpClient client, String host, String endpointUrl, String clientId, Map credentials) throws HttpFailure, IOException { + public static void invokeClientManagementRequest(KeycloakDeployment deployment, String host, String endpointUrl) throws HttpFailure, IOException { 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 formparams = new ArrayList(); formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_CLUSTER_HOST, host)); HttpPost post = new HttpPost(endpointUrl); - - 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); - } + ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); post.setEntity(form); - HttpResponse response = client.execute(post); + HttpResponse response = deployment.getClient().execute(post); int status = response.getStatusLine().getStatusCode(); if (status != 204) { HttpEntity entity = response.getEntity(); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java new file mode 100644 index 0000000000..b7e7a28e63 --- /dev/null +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java @@ -0,0 +1,19 @@ +package org.keycloak.adapters.authentication; + +import java.util.Map; + +import org.keycloak.adapters.KeycloakDeployment; + +/** + * TODO: Javadoc + * + * @author Marek Posolda + */ +public interface ClientCredentialsProvider { + + String getId(); + + void init(KeycloakDeployment deployment, Object config); + + void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formParams); +} diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java new file mode 100644 index 0000000000..df8b8809ba --- /dev/null +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java @@ -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 Marek Posolda + */ +public class ClientCredentialsProviderUtils { + + private static Logger logger = Logger.getLogger(ClientCredentialsProviderUtils.class); + + public static ClientCredentialsProvider bootstrapClientAuthenticator(KeycloakDeployment deployment) { + String clientId = deployment.getResourceName(); + Map 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 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 authenticators, ClassLoader classLoader) { + for (ClientCredentialsProvider authenticator : ServiceLoader.load(ClientCredentialsProvider.class, classLoader)) { + authenticators.put(authenticator.getId(), authenticator); + } + } + + public static void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map 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 formparams) { + Map reqHeaders = new HashMap<>(); + Map reqParams = new HashMap<>(); + setClientCredentials(deployment, reqHeaders, reqParams); + + for (Map.Entry header : reqHeaders.entrySet()) { + post.setHeader(header.getKey(), header.getValue()); + } + + for (Map.Entry param : reqParams.entrySet()) { + formparams.add(new BasicNameValuePair(param.getKey(), param.getValue())); + } + } + +} diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientIdAndSecretCredentialsProvider.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientIdAndSecretCredentialsProvider.java new file mode 100644 index 0000000000..ef39ff92ec --- /dev/null +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientIdAndSecretCredentialsProvider.java @@ -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 Marek Posolda + */ +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 requestHeaders, Map 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); + } + } +} diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java new file mode 100644 index 0000000000..e249a43231 --- /dev/null +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java @@ -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 Marek Posolda + */ +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 cfg = (Map) 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 requestHeaders, Map 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; + } +} diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java index a11e1ee4a3..6ae1ed608d 100644 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java @@ -23,6 +23,7 @@ import org.apache.http.message.BasicNameValuePair; import org.jboss.logging.Logger; import org.keycloak.OAuth2Constants; import org.keycloak.VerificationException; +import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils; import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.util.BasicAuthHelper; @@ -72,14 +73,8 @@ public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule { formparams.add(new BasicNameValuePair("username", username)); formparams.add(new BasicNameValuePair("password", password)); - if (deployment.isPublicClient()) { // if client is public access type - 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); - } + ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams); + UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); post.setEntity(form); @@ -135,15 +130,7 @@ public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule { HttpPost post = new HttpPost(logoutUri); List formparams = new ArrayList(); - if (deployment.isPublicClient()) { // if client is public access type - 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); - } - + ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams); formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken)); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); diff --git a/integration/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider b/integration/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider new file mode 100644 index 0000000000..3b2124bd4f --- /dev/null +++ b/integration/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider @@ -0,0 +1,2 @@ +org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider +org.keycloak.adapters.authentication.JWTClientCredentialsProvider \ No newline at end of file diff --git a/integration/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java b/integration/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java index c326d766c8..a0a24b27db 100644 --- a/integration/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java +++ b/integration/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java @@ -2,6 +2,9 @@ package org.keycloak.adapters; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; 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.TokenStore; import org.keycloak.util.PemUtils; @@ -32,8 +35,10 @@ public class KeycloakDeploymentBuilderTest { assertTrue(deployment.isEnableBasicAuth()); assertTrue(deployment.isExposeToken()); assertEquals("234234-234234-234234", deployment.getResourceCredentials().get("secret")); + assertEquals(ClientIdAndSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId()); assertEquals(20, ((ThreadSafeClientConnManager) deployment.getClient().getConnectionManager()).getMaxTotal()); assertEquals("https://backend:8443/auth/realms/demo/protocol/openid-connect/token", deployment.getTokenUrl()); + assertEquals(RelativeUrlsUsed.NEVER, deployment.getRelativeUrls()); assertTrue(deployment.isAlwaysRefreshToken()); assertTrue(deployment.isRegisterNodeAtStartup()); assertEquals(1000, deployment.getRegisterNodePeriod()); @@ -41,4 +46,16 @@ public class KeycloakDeploymentBuilderTest { 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()); + } + } diff --git a/integration/adapter-core/src/test/resources/keycloak-jwt.json b/integration/adapter-core/src/test/resources/keycloak-jwt.json new file mode 100644 index 0000000000..6e46f33237 --- /dev/null +++ b/integration/adapter-core/src/test/resources/keycloak-jwt.json @@ -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" + } + } +} \ No newline at end of file diff --git a/integration/adapter-core/src/test/resources/keycloak-no-credentials.json b/integration/adapter-core/src/test/resources/keycloak-no-credentials.json new file mode 100644 index 0000000000..5f223ac75f --- /dev/null +++ b/integration/adapter-core/src/test/resources/keycloak-no-credentials.json @@ -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 +} \ No newline at end of file diff --git a/integration/adapter-core/src/test/resources/keycloak.json b/integration/adapter-core/src/test/resources/keycloak.json index 32a8df3371..afa00f505a 100644 --- a/integration/adapter-core/src/test/resources/keycloak.json +++ b/integration/adapter-core/src/test/resources/keycloak.json @@ -22,8 +22,8 @@ "truststore": "classpath:/cacerts.jks", "truststore-password": "changeit", "client-keystore": "classpath:/keystore.jks", - "client-keystore-password": "changeit", - "client-key-password": "password", + "client-keystore-password": "storepass", + "client-key-password": "keypass", "auth-server-url-for-backend-requests": "https://backend:8443/auth", "always-refresh-token": true, "register-node-at-startup": true, diff --git a/integration/adapter-core/src/test/resources/keystore.jks b/integration/adapter-core/src/test/resources/keystore.jks index 0c4e3a189c8532fe7292ad75d6962bb0cb68bc7a..1d62fb22368d111498d20462f3b8f13aca933e5f 100644 GIT binary patch delta 2028 zcmVr>a`8?{CaIJoZHS6<2V-9u@mlGR|;T0 znn2urTbuNioWu^R4)qXnQ`dU*6p|{)>z7Jz)+&|7`il#ajz5Lqo#UcGU~lu1{D@{=s|6a+$d2`M11Zo zhx0gllPAk`HHPj~(_ocO(n3E#itN3)V*`d*dj-3zz8V{6waIhk>?sC=n()2V8B;O;3DGWO`33#`)7 z*yiek_RXCK`P^{5Gs10PjJU0xJ91GEu<>-qtg3B1OebZS*4W`T?nR9*FlMBB**e^R zgAA0%3}vzF-h&~et}Ppyf9)l%aIPBaPkc^HmOC(tA9q7se zYq#B8Dn`kz8qTLHFlf)cU7`fXA$z%7Y0qAjC};9Zp_|(K_C)b$iot{+R3Z`7tx}*R zn~8?4i5arhQc-1F6 zg4$)#72UP~R6!2q0^(&piGG6ENwZ}k{1v5IVKJYQH$rpl zV!CLo>yKQVVRMVBAO2ybq1?wP{r+5wzDmF|Qau*re0=PZ<J+y*B6^%Muwh(5%Jw#;?J(vIPF zIFSbJc>o<;bFg%yu&X0}=au!T&fxF39ryGv$8&cY&tzZ{Mom$W7@^~T1GG42xbYP0 z#xz1=`iHZZ;w)OW+K#@Nkudhq)iNcc5$&5802clWVd>0jJ66+>2Nc18AMI1)kpKVy z0RRP9E;TSY00089FoFWDFoFS<0tNw3PUBoSFbxI?Duzgg_YDC73k3i$8Zj6!76t=V z1Op5YV|82CaxHLga&%#AlUN2Jf2(ijeTRdDCs`t>ajTMY(N5eb z1M?O-+s^IFE$N(7xnhBi4}1OZFr=`RJh;1CxzVA!hNI?nZ(P#f!LE<4h|&pSO!=JO zCwhj${#5DN2$Sr-e;FuKvQnaOfmkma1O0I6X7IibIdf=(>D)h|sNFx`)9D)IR5!_PIgP~#c>HGV?n|G!4*$}4owy9i$s5R3%f|<>n z{x{HZKIJE+|L6u!wAqu~KgIRA

`7(wjJ|Ak&fl#4 zFjwKXAf7Co5G0xlf3iLD_00Z1@jH*@`8q}jgBF8o0av^Yr03G7SNK`~3f5b>@S5)i+gC>?TcvMsJYF0;`{FA+a=ep#^cV+d%fQr>xlm7QjU0j7O%yR}})9D1@EX KJ}WetUTc`On$3Oy literal 2249 zcmchYc{J1uAI4`RCJafEY{R(req))CL}T9)W9(~0#=gzOB}>^tlUu|nk=>NBgt#Kf zRw85xuPlwR4?;@jbvoz0=bqF5?;p<}&*!i2dCqg5@4@Q9DhLFE918qPT%lee5ndsR z?ta(YLl2=z2e;H25Qq%|#KR5%9(K4qI~WF*gYbdDY#>-XY-y$URaBV6I|k};hl6qH z)uJf!BxcTstcra7nv!GC8=&ef*TfR1k>>39ix66Olkv}c0%kA^tUFQNH!5*wok@zwqBPT{3V z-BpXvK9A&7&sYixWSEAgr|c_uU#SW_W;OlYnYfH0dDSFJ>$YMp*A5nOg*CXgt8x!3 zp4KF%Xy5wNlSmK4W5J%8#edt6 zUz;lV6=FuCWi}Bq5v{#PUTEjchs?II;)?I;Fu5JUY0+WeHTFTF7F>Nyn7XD{G ztdj`E2~thcSSf-3FXYK5h~T}#VKHAE`$;Z`Meiys-;kjHg`d)~z$ z&|7ntHp0maaRsgqb!#&O9+lgqiLIW6Jd7J?h|Lv9(;gX4;AnM9c48=IoYDgtQ-~()wkV+qs*83Dr zW&4Br`pwP&ZLH8g_T+hW&Ia>Ksgtw>+P=vdu0{?{y7$n_q2+X3M9RIDZq#_)wCZgK z6Emii>gv{WrlWS)hMl#d-ah*o|8%Eawp`rCCrw81Abwz2zgUODdK!f7lT}D6DP|&M$K5b%~8EO2q>+EY42dBHkdr>n=w%&aXCfk&LNnxn(PyIF7 zgrFCb9u$vFmVckeO5UtCcTxs1k&kwIYVC)p%W_cDXRM=njl_>@;P#}2>rUHnKTkI8 ziE~Y{DB=3W`&!S3Ps}Dc3%lBCzC?sbe+N9(E!m+PaW<2iKG_nAnmEK}J@5s?K1_B! z)8#y*-f&kNVt~9ddE&tqkh>(T1-idAB~Go^zx8HRu=SxMqql_5dsMlsEOXR2q@tUk zasBUbYytOpk(mnicFyixYx{HS9xnR#LD9yH^-yk_{pHWp&WJ>xFNvSerXDK}e0MCL zUnf%lx2O@l=_7qrgBEd)F~}*m_*FzHi0V&kR*#OG1Bv_a20ZmbO_?a1x5#jvBHnU| zV~I*<-g;m?0|}Eai030Vr^catcU(AiXxGETWoFA$-zVv9pXJokpVDw#^R|wwB$?I; z(L%Xm$Hw$*#qs*)Es2~Jsn!>H$Z<5mawKQ@@YT>cxFk;KCj1Gk%S0*wN3~D&@~epx zLAs1kc*kz_PPVKkNp#vE7qU|e@tvY}?fhL6hy(NYYB%>6cS`Vp)G(Kr ztv+|cvL={VqMxEf$y&BX%UbYC>#Hc5(F>y2OwIWzd({!SJJT7f4Zrr0%}z-T&;Ws; zkw82&0*HsWJcdHR5C|0O2LRmcoN|dqiM!lj@KH7p;D_ctTui79jGh08O#mSvkQnek z!ton9@*9EwMvneQp#Km+RFE632B3knXfyx-Dh|J_>Oc1Xd`&zU`R_Omp9M4?i~s@g zU^pZm3cxZJ+C-k;xAGMW3^<-?^`lo%Z z>$Ubhp!I{j*&%Rxav`PQPk9mly!2+?$L>aV1`6r2&9&b?<&WIV13z6E z5v&jpP%jc-DD^aR%wX;+Cmm!sBZIQ!p50Kd875uVdzoIwKBDZwoynIpzmUqAp6_&O z{d>N+sN>{M4XHbWx>>}^9N){Ii-4dLd&bfNNEW&56+Vr%2{dMBXNIx1l220kJ{o%W zpll&fFbMoa0uTd44jUrE13LjbUNNqz3#tSy+u)G?}W_e}{P3slyP#phD_% zzU2A$qpvxP1foi@$MG>QI#;vCZpiXxl*(~LpCl!Rh4o3jJy-BI$70*tNHttpeF+&u zX1giSs>{Ps`D{B}WbMmF!4aPHlB}S}tH_7-ZM-Y7AwMY+j12};!w+;N%3?G>@Vk|R zm|@2=A;bk!k^OfgOWW2g3{v7x|7t_w7;HJAc6;@ke3Gj=SDhK3y2q29rkX0F+8^!% zhP^jEdfL2snJQ1Jmy^n;ezH|uFgMIuD^wo7v{-PB*UNENC1dZDdYZNGbca>Qokn*$ zPJA^Rt;KqipAYeFl=$XAUlGH6O;C1&F*`jIGaVVs$6<|+j_=N;9G`6a!(JnhUB entry : credentials.entrySet()) { - codeForm.param(entry.getKey(), entry.getValue()); + for (Map.Entry entry : credentials.entrySet()) { + codeForm.param(entry.getKey(), (String) entry.getValue()); } Response res = client.target(tokenUrl).request().post(Entity.form(codeForm)); try { diff --git a/integration/servlet-oauth-client/pom.xml b/integration/servlet-oauth-client/pom.xml index 7d957ca7ab..b5c869ffbe 100755 --- a/integration/servlet-oauth-client/pom.xml +++ b/integration/servlet-oauth-client/pom.xml @@ -44,6 +44,27 @@ jboss-servlet-api_3.0_spec provided + + org.codehaus.jackson + jackson-core-asl + provided + + + org.codehaus.jackson + jackson-mapper-asl + provided + + + org.codehaus.jackson + jackson-xc + provided + + + org.jboss.logging + jboss-logging + ${jboss.logging.version} + provided + junit junit diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java new file mode 100644 index 0000000000..d24b0b4189 --- /dev/null +++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java @@ -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 Marek Posolda + */ +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 getCredentials() { + return deployment.getResourceCredentials(); + } + + @Override + public void setCredentials(Map 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"); + } +} diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java index 5f5daaa311..3d61013300 100755 --- a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java +++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java @@ -1,14 +1,10 @@ package org.keycloak.servlet; -import org.apache.http.client.HttpClient; -import org.keycloak.AbstractOAuthClient; import org.keycloak.OAuth2Constants; -import org.keycloak.adapters.HttpClientBuilder; import org.keycloak.adapters.ServerRequest; import org.keycloak.jose.jws.JWSInput; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; -import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.util.KeycloakUriBuilder; import org.keycloak.util.UriUtils; @@ -22,24 +18,18 @@ import java.net.URI; * @author Bill Burke * @version $Revision: 1 $ */ -public class ServletOAuthClient extends AbstractOAuthClient { - protected HttpClient client; - protected AdapterConfig adapterConfig; - - public void start() { - client = new HttpClientBuilder().build(adapterConfig); - } +public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient { /** * closes client */ public void stop() { - client.getConnectionManager().shutdown(); + getDeployment().getClient().getConnectionManager().shutdown(); } private AccessTokenResponse resolveBearerToken(HttpServletRequest request, String redirectUri, String code) throws IOException, ServerRequest.HttpFailure { // Don't send sessionId in oauth clients for now - return ServerRequest.invokeAccessCodeToToken(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 { - return ServerRequest.invokeRefresh(client, publicClient, refreshToken, getUrl(request, tokenUrl, false), clientId, credentials); + return ServerRequest.invokeRefresh(getDeployment(), refreshToken); } public static IDToken extractIdToken(String idToken) { @@ -167,8 +157,4 @@ public class ServletOAuthClient extends AbstractOAuthClient { return url; } } - - public void setAdapterConfig(AdapterConfig adapterConfig) { - this.adapterConfig = adapterConfig; - } } diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClientBuilder.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClientBuilder.java index 20f33bcc30..4354f1040d 100755 --- a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClientBuilder.java +++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClientBuilder.java @@ -1,14 +1,8 @@ package org.keycloak.servlet; -import org.apache.http.client.HttpClient; -import org.keycloak.constants.ServiceUrlConstants; -import org.keycloak.adapters.HttpClientBuilder; -import org.keycloak.enums.RelativeUrlsUsed; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.KeycloakDeploymentBuilder; 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; /** @@ -18,62 +12,21 @@ import java.io.InputStream; public class ServletOAuthClientBuilder { public static ServletOAuthClient build(InputStream is) { - AdapterConfig adapterConfig = getAdapterConfig(is); - return build(adapterConfig); - } - - public static AdapterConfig getAdapterConfig(InputStream is) { - try { - return JsonSerialization.readValue(is, AdapterConfig.class, true); - } catch (IOException e) { - throw new RuntimeException(e); - } + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(is); + ServletOAuthClient client = new ServletOAuthClient(); + client.setDeployment(deployment); + return client; } public static ServletOAuthClient build(AdapterConfig adapterConfig) { - ServletOAuthClient oauthClient = new ServletOAuthClient(); - build(adapterConfig, oauthClient); - return oauthClient; + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(adapterConfig); + ServletOAuthClient client = new ServletOAuthClient(); + client.setDeployment(deployment); + return client; } public static void build(InputStream is, ServletOAuthClient oauthClient) { - build(getAdapterConfig(is), oauthClient); - } - - - 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; - } + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(is); + oauthClient.setDeployment(deployment); } } diff --git a/integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java b/integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java new file mode 100644 index 0000000000..604b58cfe8 --- /dev/null +++ b/integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java @@ -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 Marek Posolda + */ +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()); + } +} diff --git a/integration/servlet-oauth-client/src/test/resources/keycloak.json b/integration/servlet-oauth-client/src/test/resources/keycloak.json new file mode 100644 index 0000000000..d952196c03 --- /dev/null +++ b/integration/servlet-oauth-client/src/test/resources/keycloak.json @@ -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" +} \ No newline at end of file diff --git a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java index 8b14f74593..9992af3e52 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java +++ b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java @@ -304,7 +304,7 @@ public class DefaultAuthenticationFlows { execution = new AuthenticationExecutionModel(); execution.setParentFlow(clients.getId()); execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); - execution.setAuthenticator("client-signed-jwt"); + execution.setAuthenticator("client-jwt"); execution.setPriority(20); execution.setAuthenticatorFlow(false); realm.addAuthenticatorExecution(execution); diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index f3dd3b8072..b51a39fe07 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -33,6 +33,7 @@ import org.keycloak.util.Time; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.net.URI; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -67,6 +68,7 @@ public class AuthenticationProcessor { // Used for client authentication protected ClientModel client; + protected Map clientAuthAttributes = new HashMap<>(); public AuthenticationProcessor() { } @@ -83,6 +85,10 @@ public class AuthenticationProcessor { this.client = client; } + public Map getClientAuthAttributes() { + return clientAuthAttributes; + } + public ClientSessionModel getClientSession() { return clientSession; } @@ -341,6 +347,11 @@ public class AuthenticationProcessor { AuthenticationProcessor.this.setClient(client); } + @Override + public Map getClientAuthAttributes() { + return AuthenticationProcessor.this.getClientAuthAttributes(); + } + @Override public ClientSessionModel getClientSession() { return AuthenticationProcessor.this.getClientSession(); diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java index 3ef5c28c7d..7c864f10d4 100644 --- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java @@ -55,20 +55,6 @@ public class ClientAuthenticationFlow implements AuthenticationFlow { AuthenticationFlow authenticationFlow; 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(); if (flowChallenge == null) { if (model.isAlternative()) alternativeSuccessful = true; diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java index 1d20325cc9..9f11eb97fc 100644 --- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java +++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java @@ -1,5 +1,7 @@ package org.keycloak.authentication; +import java.util.Map; + import org.keycloak.models.ClientModel; /** @@ -22,4 +24,15 @@ public interface ClientAuthenticationFlowContext extends AbstractAuthenticationF */ 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 getClientAuthAttributes(); + } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java index 8fd5d361e5..dc232e3584 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java @@ -29,51 +29,4 @@ public class ClientAuthUtil { 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 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; - } } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java index 00bd47f85b..8bbf1905da 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java @@ -8,6 +8,7 @@ import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import org.jboss.logging.Logger; +import org.keycloak.OAuth2Constants; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.ClientAuthenticationFlowContext; @@ -39,21 +40,60 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator @Override public void authenticateClient(ClientAuthenticationFlowContext context) { - ClientModel client = ClientAuthUtil.getClientFromClientId(context); - if (client == null) { - return; - } else { - context.setClient(client); + String client_id = null; + String clientSecret = null; + + String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); + MultivaluedMap 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 if (client.isPublicClient()) { context.success(); return; } - String clientSecret = getClientSecret(context); - if (clientSecret == null) { Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client secret not provided in request"); context.challenge(challengeResponse); @@ -75,30 +115,6 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator context.success(); } - protected String getClientSecret(ClientAuthenticationFlowContext context) { - String clientSecret = null; - String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); - MultivaluedMap 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 public String getDisplayType() { return "Client Id and Secret"; diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java index 3a98c8d33b..3f7ddb6c15 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java @@ -30,7 +30,7 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator { 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 AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ValidateClientId.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ValidateClientId.java deleted file mode 100644 index 8b6a6b280f..0000000000 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/ValidateClientId.java +++ /dev/null @@ -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 Marek Posolda - */ -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 getConfigProperties() { - return new LinkedList<>(); - } - - @Override - public String getId() { - return PROVIDER_ID; - } -} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java deleted file mode 100644 index 2c038d4fbd..0000000000 --- a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java +++ /dev/null @@ -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 Marek Posolda - */ -public class ServiceAccountManager { - - protected static final Logger logger = Logger.getLogger(ServiceAccountManager.class); - - private TokenManager tokenManager; - private EventBuilder event; - private HttpRequest request; - private MultivaluedMap 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 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(); - } - -} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java index 1c7469976b..bec6aafd35 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java @@ -190,7 +190,7 @@ public class LogoutEndpoint { } private ClientModel authorizeClient() { - ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm); + ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm).getClient(); if (client.isBearerOnly()) { throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index dd7235fd95..15c1f4689d 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -7,6 +7,7 @@ import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.constants.AdapterConstants; +import org.keycloak.constants.ServiceAccountConstants; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; @@ -19,17 +20,16 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; 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.ServiceAccountManager; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.Cors; import org.keycloak.services.Urls; @@ -52,6 +52,7 @@ public class TokenEndpoint { private static final Logger logger = Logger.getLogger(TokenEndpoint.class); private MultivaluedMap formParams; private ClientModel client; + private Map clientAuthAttributes; private enum Action { AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS @@ -144,7 +145,9 @@ public class TokenEndpoint { } 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()) { throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST); @@ -237,6 +240,7 @@ public class TokenEndpoint { } updateClientSession(clientSession); + updateUserSessionFromClientAuth(userSession); 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()); updateClientSessions(userSession.getClientSessions()); + updateUserSessionFromClientAuth(userSession); } catch (OAuthErrorException e) { event.error(Errors.INVALID_TOKEN); @@ -312,6 +317,12 @@ public class TokenEndpoint { } } + private void updateUserSessionFromClientAuth(UserSessionModel userSession) { + for (Map.Entry attr : clientAuthAttributes.entrySet()) { + userSession.setNote(attr.getKey(), attr.getValue()); + } + } + public Response buildResourceOwnerPasswordCredentialsGrant() { event.detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token"); @@ -349,6 +360,7 @@ public class TokenEndpoint { } processor.attachSession(); UserSessionModel userSession = processor.getUserSession(); + updateUserSessionFromClientAuth(userSession); AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession) .generateAccessToken(session, scope, client, user, userSession, clientSession) @@ -363,8 +375,68 @@ public class TokenEndpoint { } public Response buildClientCredentialsGrant() { - ServiceAccountManager serviceAccountManager = new ServiceAccountManager(tokenManager, event, request, formParams, session, client); - return serviceAccountManager.buildClientCredentialsGrant(); + 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); + } + + 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(); } } \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java index a2b701cadf..bc7fdb3e8d 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java @@ -1,5 +1,7 @@ package org.keycloak.protocol.oidc.utils; +import java.util.Map; + import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.events.EventBuilder; @@ -17,7 +19,7 @@ import javax.ws.rs.core.Response; */ 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(); 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); } - return client; + return new ClientAuthResult(client, processor.getClientAuthAttributes()); + } + + public static class ClientAuthResult { + + private final ClientModel client; + private final Map clientAuthAttributes; + + private ClientAuthResult(ClientModel client, Map clientAuthAttributes) { + this.client = client; + this.clientAuthAttributes = clientAuthAttributes; + } + + public ClientModel getClient() { + return client; + } + + public Map getClientAuthAttributes() { + return clientAuthAttributes; + } } } diff --git a/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java index 2b92ccf47a..4e7d7de30c 100755 --- a/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java +++ b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java @@ -153,7 +153,7 @@ public class ClientsManagementService { } protected ClientModel authorizeClient() { - ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm); + ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm).getClient(); if (client.isPublicClient()) { Map error = new HashMap(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java index b65cac67f2..c3f04bd85d 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java @@ -1,5 +1,6 @@ package org.keycloak.testsuite.oauth; +import java.security.PrivateKey; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedList; @@ -17,7 +18,7 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; 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.constants.ServiceAccountConstants; import org.keycloak.constants.ServiceUrlConstants; @@ -238,8 +239,8 @@ public class ClientAuthSignedJWTTest { @Test public void testAssertionMissingIssuer() throws Exception { - String invalidJwt = ClientAuthAdapterUtils.createSignedJWT(null, getRealmInfoUrl(), - "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10); + String invalidJwt = getClientSignedJWT( + "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, null); List parameters = new LinkedList(); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); @@ -254,8 +255,8 @@ public class ClientAuthSignedJWTTest { @Test public void testAssertionUnknownClient() throws Exception { - String invalidJwt = ClientAuthAdapterUtils.createSignedJWT("unknown-client", getRealmInfoUrl(), - "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10); + String invalidJwt = getClientSignedJWT( + "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, "unknown-client"); List parameters = new LinkedList(); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); @@ -339,24 +340,8 @@ public class ClientAuthSignedJWTTest { @Test public void testAssertionInvalidSignature() throws Exception { // JWT for client1, but signed by privateKey of client2 - String invalidJwt = ClientAuthAdapterUtils.createSignedJWT("client1", getRealmInfoUrl(), - "classpath:client-auth-test/keystore-client2.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10); - - List parameters = new LinkedList(); - 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); + String invalidJwt = getClientSignedJWT( + "classpath:client-auth-test/keystore-client2.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, "client1"); List parameters = new LinkedList(); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); @@ -491,17 +476,20 @@ public class ClientAuthSignedJWTTest { private String getClient1SignedJWT() { - return ClientAuthAdapterUtils.createSignedJWT("client1", getRealmInfoUrl(), - "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10); + return getClientSignedJWT("classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, "client1"); } private String getClient2SignedJWT() { - // keystore-client2.p12 doesn't work on Sun JDK due to restrictions on key length - // String keystoreFile = "classpath:client-auth-test/keystore-client2.p12"; + return getClientSignedJWT("classpath:client-auth-test/keystore-client2.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, "client2"); + } - String keystoreFile = "classpath:client-auth-test/keystore-client2.jks"; - return ClientAuthAdapterUtils.createSignedJWT("client2", getRealmInfoUrl(), - keystoreFile, "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10); + private String getClientSignedJWT(String keystoreFile, String storePassword, String keyPassword, String keyAlias, KeystoreUtil.KeystoreFormat format, String clientId) { + PrivateKey privateKey = KeystoreUtil.loadPrivateKeyFromKeystore(keystoreFile, storePassword, keyPassword, keyAlias, format); + + JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider(); + jwtProvider.setPrivateKey(privateKey); + jwtProvider.setTokenTimeout(10); + return jwtProvider.createSignedRequestToken(clientId, getRealmInfoUrl()); } private String getRealmInfoUrl() { diff --git a/testsuite/integration/src/test/resources/adapter-test/demorealm.json b/testsuite/integration/src/test/resources/adapter-test/demorealm.json index 0e47e0c554..af9a559718 100755 --- a/testsuite/integration/src/test/resources/adapter-test/demorealm.json +++ b/testsuite/integration/src/test/resources/adapter-test/demorealm.json @@ -126,7 +126,9 @@ "redirectUris": [ "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", diff --git a/testsuite/integration/src/test/resources/adapter-test/secure-portal-keycloak.json b/testsuite/integration/src/test/resources/adapter-test/secure-portal-keycloak.json index c479feb441..3500790725 100755 --- a/testsuite/integration/src/test/resources/adapter-test/secure-portal-keycloak.json +++ b/testsuite/integration/src/test/resources/adapter-test/secure-portal-keycloak.json @@ -5,6 +5,13 @@ "auth-server-url" : "http://localhost:8081/auth", "ssl-required" : "external", "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 + } + } } diff --git a/testsuite/integration/src/test/resources/adapter-test/secure-portal-keystore.jks b/testsuite/integration/src/test/resources/adapter-test/secure-portal-keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..9b2a4d6753db04b0eed4be54134800c7be59d058 GIT binary patch literal 2028 zcmV?yV%b+X<$6B72zV^b(@+A>)zJ^ zKO=-5?_`f~=}x8FF`K{C1KUJvm$RS;Cz9anI{KZ2sI2GrCPz!i(1OiP+E($ZoJ30G z2Xkyy;mj|6rli?%&Fzu%*7)xb z`*l9ciq7Rg$Eh}3msS7$K;nS%M<8zsolEFYQM!Dmcjfa)<5lAu6wD>HGtzB)hvDF1 zgD;%e=tX;qi&Eqq9g?Z$*x$W(V`6+Sb~t3y?dSP)Q6}z$(5fO|Et*o$7uz6&W(}b; z^vhaxGkbAi1KyA!j?nqI`ZhlMkrQEkMwG!wA7rmoXG8dCJ1(XW46j!dWs~5c<*hmj zeu@%BWGSHa6@(HR7e z!ZMrO9D8WZEnfcLL`v(GFPow2YV0+wky-1nxj<8zd5OZ(q9O&qQukKQI1EyXaJX$M zY5oQ)w2HfBwSvgxQ}q!ILoymqP{K`Nk;NL-7IX z>g3J~QUbLckPyrB4(axOiMeUm?wJc6VyFDMUjeCi~} zuS#2( z+FG#eO?QP^ph&@O5`+-v^BoCR{L|46F|ph9IydbUc6RC^tJr z5(g9v6dT2$ZRBk;bwV@OWJqGzCrO)#%HDw^M#R z`BwP67EA1rN5k2D=A~Ppdn5lPDNp)m3ot&#H#v|objX_46x>>D*KGpH3pO2XKWucj z3v#owA+WiJumt@W&`Q(_DoqR<%>&<_&^_=f`FC3=5#YlcVHCa9%%7K$xV->v-fJ{3 zq`UI?M)IPp6aSaN0JbhI#C=4PSfrw4cq=tesC$HC0XsGok*cz5&8t2;)}LK|q>0f& zGXs;Nxn?_-f?pBayi7;vVws+$dQ!bA-`?cIX>gF16Sf+4tvGre$Z}Yd*|98cWf}r3 zIyY(g-S%dg{5y|OGO^=r`5{KQhJ%Q1uC<^L<*5ge{u6cgP-rkZ+^h_xb{`%K{-n4( zNbN5JS7$snRqHc{pUuWlgUv|_Yc}f=$a!I?7l8ax6vkL;6S|}i!huDN@o5IyJ|yz000311z0XMFgXAK0--R10-Z2|0fhnv z0Z%-z+D|YI1_>&LNQUU9~12ZQQ1P~CTL5yAdfpQx5}T5?dMrz8BA47go#;b})p3^cF8^t*nADo+%< zvks^&J^<*pQ!$8g77XTlgWupigI8?ZHY0utXRS4+<&G|s#$2#J%53!C|GRr}1>&VvtilN0Vl8AVZ}Ue3p@UQ~SXKY3Yu$I} z%f($0=~|Hv8T&!%c5@sI#Jd6m0RRCo4F(A+hDe6@4FLfQ1potr0RaFgPL2bPtbc7A z&%)V0h>fzOa0Ni*Xu+3$dK>P~;U$s?z%endW zF)nweIgS_eCMHayWjsaqBQ?6s(b$=nc){qj2*5DV}ox literal 0 HcmV?d00001 diff --git a/testsuite/tomcat6/src/test/resources/adapter-test/secure-portal/WEB-INF/keycloak.json b/testsuite/tomcat6/src/test/resources/adapter-test/secure-portal/WEB-INF/keycloak.json index dd38f249c2..8df199b1d8 100755 --- a/testsuite/tomcat6/src/test/resources/adapter-test/secure-portal/WEB-INF/keycloak.json +++ b/testsuite/tomcat6/src/test/resources/adapter-test/secure-portal/WEB-INF/keycloak.json @@ -5,6 +5,13 @@ "auth-server-url" : "http://localhost:8080/auth", "ssl-required" : "external", "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 + } + } } From b0e2624343afed04a238e560907d66a753a80463 Mon Sep 17 00:00:00 2001 From: mposolda Date: Fri, 21 Aug 2015 09:49:19 +0200 Subject: [PATCH 4/4] KEYCLOAK-1295 Fixes and javadoc --- .../admin/resources/js/controllers/clients.js | 6 +- .../ClientCredentialsProvider.java | 44 ++++++- .../ClientCredentialsProviderUtils.java | 3 + .../JWTClientCredentialsProvider.java | 3 +- ...KeycloakDeploymentDelegateOAuthClient.java | 6 +- .../keycloak/servlet/ServletOAuthClient.java | 110 ++++++++++++++++-- .../ServletOAuthClientBuilderTest.java | 4 +- .../ClientAuthenticationFlowContext.java | 1 + .../authentication/ClientAuthenticator.java | 13 ++- .../ClientAuthenticatorFactory.java | 8 +- .../ClientIdAndSecretAuthenticator.java | 4 +- .../client/JWTClientAuthenticator.java | 6 + 12 files changed, 185 insertions(+), 23 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index c6924c9c6f..680684561b 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -74,18 +74,18 @@ module.controller('ClientSignedJWTCtrl', function($scope, $location, realm, clie $scope.realm = realm; $scope.client = client; - var signingKeyInfo = ClientCertificate.get({ realm : realm.realm, client : client.id, attribute: 'jwt.credentials' }, + var signingKeyInfo = ClientCertificate.get({ realm : realm.realm, client : client.id, attribute: 'jwt.credential' }, function() { $scope.signingKeyInfo = signingKeyInfo; } ); $scope.importCertificate = function() { - $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/import/jwt.credentials"); + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/import/jwt.credential"); }; $scope.generateSigningKey = function() { - $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/export/jwt.credentials"); + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/export/jwt.credential"); }; $scope.cancel = function() { diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java index b7e7a28e63..38b0e038ff 100644 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java @@ -5,15 +5,57 @@ import java.util.Map; import org.keycloak.adapters.KeycloakDeployment; /** - * TODO: Javadoc + * The simple SPI for authenticating clients/applications . It's used by adapter during all OIDC backchannel requests to Keycloak server + * (codeToToken exchange, refresh token or backchannel logout) . You can also use it in your application during direct access grants or service account request + * (See the service-account example from Keycloak demo for more info) + * + * When you implement this SPI on the adapter (application) side, you also need to implement {@link org.keycloak.authentication.ClientAuthenticator} on the server side, + * so your server is able to authenticate client + * + * You must specify a file + * META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider in the WAR that this class is contained in (or in the JAR that is attached to the WEB-INF/lib or as jboss module + * if you want to share the implementation among more WARs). This file must have the fully qualified class name of all your ClientAuthenticatorFactory classes + * + * NOTE: The SPI is not finished and method signatures are still subject to change in future versions (for example to support usecase for + * authentication with client certificate) + * + * @see ClientIdAndSecretCredentialsProvider + * @see JWTClientCredentialsProvider * * @author Marek Posolda */ public interface ClientCredentialsProvider { + /** + * Return the ID of the provider. Use this ID in the keycloak.json configuration as the subelement of the "credentials" element + * + * For example if your provider has ID "kerberos-keytab" , use the configuration like this in keycloak.json + * + * "credentials": { + * + * "kerberos-keytab": { + * "keytab": "/tmp/foo" + * } + * } + * + * @return + */ String getId(); + /** + * Called by adapter during deployment of your application. You can for example read configuration and init your authenticator here + * + * @param deployment the adapter configuration + * @param config the configuration of your provider read from keycloak.json . For the kerberos-keytab example above, it will return map with the single key "keytab" with value "/tmp/foo" + */ void init(KeycloakDeployment deployment, Object config); + /** + * Called every time adapter needs to perform backchannel request + * + * @param deployment Fully resolved deployment + * @param requestHeaders You should put any HTTP request headers you want to use for authentication of client. These headers will be attached to the HTTP request sent to Keycloak server + * @param formParams You should put any request parameters you want to use for authentication of client. These parameters will be attached to the HTTP request sent to Keycloak server + */ void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formParams); } diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java index df8b8809ba..be3eb41e55 100644 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java @@ -60,6 +60,9 @@ public class ClientCredentialsProviderUtils { } } + /** + * Use this method when calling backchannel request directly from your application. See service-account example from demo for more details + */ public static void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formparams) { ClientCredentialsProvider authenticator = deployment.getClientAuthenticator(); authenticator.setClientCredentials(deployment, requestHeaders, formparams); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java index e249a43231..6503e678d9 100644 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java @@ -12,7 +12,8 @@ import org.keycloak.util.KeystoreUtil; import org.keycloak.util.Time; /** - * Client authentication based on JWT signed by client private key + * Client authentication based on JWT signed by client private key . + * See specs for more details. * * @author Marek Posolda */ diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java index d24b0b4189..fc2445ba46 100644 --- a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java +++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java @@ -4,7 +4,9 @@ import java.util.Map; import org.keycloak.AbstractOAuthClient; import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.enums.RelativeUrlsUsed; +import org.keycloak.util.KeycloakUriBuilder; /** * @author Marek Posolda @@ -43,7 +45,7 @@ public class KeycloakDeploymentDelegateOAuthClient extends AbstractOAuthClient { @Override public String getAuthUrl() { - return deployment.getAuthUrl().clone().build().toString(); + throw new IllegalStateException("Illegal to call this method. Use KeycloakDeployment to resolve correct deployment for this request"); } @Override @@ -53,7 +55,7 @@ public class KeycloakDeploymentDelegateOAuthClient extends AbstractOAuthClient { @Override public String getTokenUrl() { - return deployment.getTokenUrl(); + throw new IllegalStateException("Illegal to call this method. Use KeycloakDeployment to resolve correct deployment for this request"); } @Override diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java index 3d61013300..72501acbf9 100755 --- a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java +++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java @@ -1,18 +1,26 @@ package org.keycloak.servlet; +import org.keycloak.KeycloakSecurityContext; import org.keycloak.OAuth2Constants; +import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.HttpFacade; +import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.ServerRequest; +import org.keycloak.enums.RelativeUrlsUsed; import org.keycloak.jose.jws.JWSInput; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; import org.keycloak.util.KeycloakUriBuilder; import org.keycloak.util.UriUtils; +import javax.security.cert.X509Certificate; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.InputStream; import java.net.URI; +import java.util.List; /** * @author Bill Burke @@ -29,7 +37,8 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient { private AccessTokenResponse resolveBearerToken(HttpServletRequest request, String redirectUri, String code) throws IOException, ServerRequest.HttpFailure { // Don't send sessionId in oauth clients for now - return ServerRequest.invokeAccessCodeToToken(getDeployment(), code, redirectUri, null); + KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request); + return ServerRequest.invokeAccessCodeToToken(resolvedDeployment, code, redirectUri, null); } /** @@ -62,10 +71,12 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient { */ public void redirect(String redirectUri, HttpServletRequest request, HttpServletResponse response) throws IOException { String state = getStateCode(); + KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request); + String authUrl = resolvedDeployment.getAuthUrl().clone().build().toString(); - KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(getUrl(request, authUrl, true)) + KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(authUrl) .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE) - .queryParam(OAuth2Constants.CLIENT_ID, clientId) + .queryParam(OAuth2Constants.CLIENT_ID, getClientId()) .queryParam(OAuth2Constants.REDIRECT_URI, redirectUri) .queryParam(OAuth2Constants.STATE, state); if (scope != null) { @@ -136,7 +147,8 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient { } public AccessTokenResponse refreshToken(HttpServletRequest request, String refreshToken) throws IOException, ServerRequest.HttpFailure { - return ServerRequest.invokeRefresh(getDeployment(), refreshToken); + KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request); + return ServerRequest.invokeRefresh(resolvedDeployment, refreshToken); } public static IDToken extractIdToken(String idToken) { @@ -149,12 +161,90 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient { } } - private String getUrl(HttpServletRequest request, String url, boolean isBrowserRequest) { - if (relativeUrlsUsed.useRelative(isBrowserRequest)) { - String baseUrl = UriUtils.getOrigin(request.getRequestURL().toString()); - return baseUrl + url; - } else { - return url; + private KeycloakDeployment resolveDeployment(KeycloakDeployment baseDeployment, HttpServletRequest request) { + ServletFacade facade = new ServletFacade(request); + return new AdapterDeploymentContext(baseDeployment).resolveDeployment(facade); + } + + + public static class ServletFacade implements HttpFacade { + + private final HttpServletRequest servletRequest; + + private ServletFacade(HttpServletRequest servletRequest) { + this.servletRequest = servletRequest; + } + + @Override + public KeycloakSecurityContext getSecurityContext() { + throw new IllegalStateException("Not yet implemented"); + } + + @Override + public Request getRequest() { + return new Request() { + + @Override + public String getMethod() { + return servletRequest.getMethod(); + } + + @Override + public String getURI() { + return servletRequest.getRequestURL().toString(); + } + + @Override + public boolean isSecure() { + return servletRequest.isSecure(); + } + + @Override + public String getQueryParamValue(String param) { + return servletRequest.getParameter(param); + } + + @Override + public Cookie getCookie(String cookieName) { + // TODO + return null; + } + + @Override + public String getHeader(String name) { + return servletRequest.getHeader(name); + } + + @Override + public List getHeaders(String name) { + // TODO + return null; + } + + @Override + public InputStream getInputStream() { + try { + return servletRequest.getInputStream(); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + + @Override + public String getRemoteAddr() { + return servletRequest.getRemoteAddr(); + } + }; + } + + @Override + public Response getResponse() { + throw new IllegalStateException("Not yet implemented"); + } + + @Override + public X509Certificate[] getCertificateChain() { + throw new IllegalStateException("Not yet implemented"); } } } diff --git a/integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java b/integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java index 604b58cfe8..d8f78e2744 100644 --- a/integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java +++ b/integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java @@ -15,8 +15,8 @@ public class ServletOAuthClientBuilderTest { @Test public void testBuilder() { ServletOAuthClient oauthClient = ServletOAuthClientBuilder.build(getClass().getResourceAsStream("/keycloak.json")); - Assert.assertEquals("https://localhost:8443/auth/realms/demo/protocol/openid-connect/auth", oauthClient.getAuthUrl()); - Assert.assertEquals("https://backend:8443/auth/realms/demo/protocol/openid-connect/token", oauthClient.getTokenUrl()); + Assert.assertEquals("https://localhost:8443/auth/realms/demo/protocol/openid-connect/auth", oauthClient.getDeployment().getAuthUrl().clone().build().toString()); + Assert.assertEquals("https://backend:8443/auth/realms/demo/protocol/openid-connect/token", oauthClient.getDeployment().getTokenUrl()); assertEquals(RelativeUrlsUsed.NEVER, oauthClient.getRelativeUrlsUsed()); Assert.assertEquals("customer-portal", oauthClient.getClientId()); Assert.assertEquals("234234-234234-234234", oauthClient.getCredentials().get(CredentialRepresentation.SECRET)); diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java index 9f11eb97fc..3dba809272 100644 --- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java +++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java @@ -5,6 +5,7 @@ import java.util.Map; import org.keycloak.models.ClientModel; /** + * Encapsulates information about the execution in ClientAuthenticationFlow * * @author Marek Posolda */ diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java index 4cbf2ef349..47c0d5de3e 100644 --- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java @@ -6,12 +6,23 @@ import org.keycloak.models.RealmModel; import org.keycloak.provider.Provider; /** + * This interface is for users that want to add custom client authenticators to an authentication flow. + * You must implement this interface as well as a ClientAuthenticatorFactory. + * + * This interface is for verifying client credentials from request. On the adapter side, you must also implement org.keycloak.adapters.authentication.ClientCredentialsProvider , which is supposed + * to add the client credentials to the request, which will ClientAuthenticator verify on server side + * + * @see org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator + * @see org.keycloak.authentication.authenticators.client.JWTClientAuthenticator + * * @author Marek Posolda */ public interface ClientAuthenticator extends Provider { /** - * TODO: javadoc + * Initial call for the authenticator. This method should check the current HTTP request to determine if the request + * satisfies the ClientAuthenticator's requirements. If it doesn't, it should send back a challenge response by calling + * the ClientAuthenticationFlowContext.challenge(Response). * * @param context */ diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java index 9f9cc86d81..18ddaa5517 100644 --- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java @@ -3,7 +3,11 @@ package org.keycloak.authentication; import org.keycloak.provider.ProviderFactory; /** - * TODO + * Factory for creating ClientAuthenticator instances. This is a singleton and created when Keycloak boots. + * + * You must specify a file + * META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory in the jar that this class is contained in + * This file must have the fully qualified class name of all your ClientAuthenticatorFactory classes * * @author Marek Posolda */ @@ -19,7 +23,7 @@ public interface ClientAuthenticatorFactory extends ProviderFactoryMarek Posolda */ diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java index 3f7ddb6c15..db0cb9fcec 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java @@ -24,6 +24,12 @@ import org.keycloak.representations.JsonWebToken; import org.keycloak.services.Urls; /** + * Client authentication based on JWT signed by client private key . + * See specs for more details. + * + * This is server side, which verifies JWT from client_assertion parameter, where the assertion was created on adapter side by + * org.keycloak.adapters.authentication.JWTClientCredentialsProvider + * * @author Marek Posolda */ public class JWTClientAuthenticator extends AbstractClientAuthenticator {