diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml index 6139c3d931..b27e9bd616 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml @@ -73,13 +73,14 @@ - + - + + @@ -104,10 +105,11 @@ - - + + + diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml index 8609f07a5e..eff01c4ce4 100755 --- a/connections/jpa/src/main/resources/META-INF/persistence.xml +++ b/connections/jpa/src/main/resources/META-INF/persistence.xml @@ -18,6 +18,7 @@ org.keycloak.models.jpa.entities.UserRoleMappingEntity org.keycloak.models.jpa.entities.ScopeMappingEntity org.keycloak.models.jpa.entities.IdentityProviderEntity + org.keycloak.models.jpa.entities.ClientIdentityProviderMappingEntity org.keycloak.models.jpa.entities.ClaimTypeEntity org.keycloak.models.jpa.entities.ProtocolMapperEntity diff --git a/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java index 9ca74f07d0..6537fc25df 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java @@ -2,7 +2,6 @@ package org.keycloak.representations.idm; import java.util.List; import java.util.Map; -import java.util.Set; /** * @author Bill Burke @@ -29,7 +28,7 @@ public class ApplicationRepresentation { protected Boolean fullScopeAllowed; protected Integer nodeReRegistrationTimeout; protected Map registeredNodes; - protected List allowedIdentityProviders; + protected List identityProviders; protected List protocolMappers; public String getId() { @@ -192,12 +191,12 @@ public class ApplicationRepresentation { this.frontchannelLogout = frontchannelLogout; } - public List getAllowedIdentityProviders() { - return this.allowedIdentityProviders; + public List getIdentityProviders() { + return this.identityProviders; } - public void setAllowedIdentityProviders(List allowedIdentityProviders) { - this.allowedIdentityProviders = allowedIdentityProviders; + public void setIdentityProviders(List identityProviders) { + this.identityProviders = identityProviders; } public List getProtocolMappers() { diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientIdentityProviderMappingRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientIdentityProviderMappingRepresentation.java new file mode 100644 index 0000000000..fdc02d3117 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/ClientIdentityProviderMappingRepresentation.java @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source + * + * Copyright 2013 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.representations.idm; + +/** + * @author pedroigor + */ +public class ClientIdentityProviderMappingRepresentation { + + protected String id; + protected boolean retrieveToken; + + public String getId() { + return this.id; + } + + public void setId(String identityProviderId) { + this.id = identityProviderId; + } + + public boolean isRetrieveToken() { + return this.retrieveToken; + } + + public void setRetrieveToken(boolean retrieveToken) { + this.retrieveToken = retrieveToken; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/OAuthClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OAuthClientRepresentation.java index aa095a7000..c12a9ebe60 100755 --- a/core/src/main/java/org/keycloak/representations/idm/OAuthClientRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/OAuthClientRepresentation.java @@ -2,7 +2,6 @@ package org.keycloak.representations.idm; import java.util.List; import java.util.Map; -import java.util.Set; /** * @author Bill Burke @@ -23,8 +22,8 @@ public class OAuthClientRepresentation { protected Boolean directGrantsOnly; protected Boolean fullScopeAllowed; protected Boolean frontchannelLogout; - protected List allowedIdentityProviders; protected List protocolMappers; + private List identityProviders; public String getId() { @@ -139,12 +138,12 @@ public class OAuthClientRepresentation { this.frontchannelLogout = frontchannelLogout; } - public List getAllowedIdentityProviders() { - return this.allowedIdentityProviders; + public List getIdentityProviders() { + return this.identityProviders; } - public void setAllowedIdentityProviders(List allowedIdentityProviders) { - this.allowedIdentityProviders = allowedIdentityProviders; + public void setIdentityProviders(List identityProviders) { + this.identityProviders = identityProviders; } public List getProtocolMappers() { diff --git a/docbook/reference/en/en-US/master.xml b/docbook/reference/en/en-US/master.xml index a2fbacb571..6bbd3010c9 100755 --- a/docbook/reference/en/en-US/master.xml +++ b/docbook/reference/en/en-US/master.xml @@ -32,6 +32,7 @@ + @@ -119,6 +120,7 @@ This one is short &AdminApi; &Events; &UserFederation; + &Kerberos; &ExportImport; &ServerCache; &SAML; diff --git a/docbook/reference/en/en-US/modules/identity-broker.xml b/docbook/reference/en/en-US/modules/identity-broker.xml index 6ccc6303c5..233afed979 100755 --- a/docbook/reference/en/en-US/modules/identity-broker.xml +++ b/docbook/reference/en/en-US/modules/identity-broker.xml @@ -962,7 +962,7 @@ Authorization: Bearer {keycloak_access_token}]]>
- Enabling/Disabling Identity Providers for Service Providers + Configuring Identity Providers for Applications By default, all identity providers enabled for a particular realm are also available to all its applications. However, you can also specify which identity providers should be available when @@ -993,6 +993,10 @@ Authorization: Bearer {keycloak_access_token}]]> + + From this page you can also configure if an application is allowed to retrieve tokens from an specific identity provider. For that, + just click on the Can Retrieve Token button. +
diff --git a/docbook/reference/en/en-US/modules/kerberos.xml b/docbook/reference/en/en-US/modules/kerberos.xml new file mode 100644 index 0000000000..24b5d9baee --- /dev/null +++ b/docbook/reference/en/en-US/modules/kerberos.xml @@ -0,0 +1,231 @@ + + Kerberos brokering + + Keycloak supports login with Kerberos ticket through SPNEGO. SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) is used + to authenticate transparently through the web browser after the user has been authenticated when logging-in his session. + For non-web cases or when ticket is not available during login, Keycloak also supports login with Kerberos username/password. + + + A typical use case for web authentication is the following: + + + + User logs into his desktop (Such as a Windows machine in Active Directory domain or Linux machine with Kerberos integration enabled). + + + + + User then uses his browser (IE/Firefox/Chrome) to access a web application secured by Keycloak. + + + + + Application redirects to Keycloak login. + + + + + Keycloak sends HTML login screen together with status 401 and HTTP header WWW-Authenticate: Negotiate + + + + + In case that browser has Kerberos ticket from desktop login, it transfers the desktop sign on information to the + Keycloak in header Authorization: Negotiate 'spnego-token' . Otherwise it just displays login screen. + + + + + Keycloak validates token from browser and authenticate user. It provisions user data from LDAP (in case of + LDAPFederationProvider with Kerberos authentication support) or let user to update his profile and prefill data + (in case of KerberosFederationProvider). + + + + + Keycloak returns back to the application. Communication between Keycloak and application happens through OpenID + Connect or SAML messages. The fact that Keycloak was authenticated through Kerberos is hidden from the application. + So Keycloak acts as broker to Kerberos/SPNEGO login. + + + + + + For setup there are 3 main parts: + + + + Setup and configuration of Kerberos server (KDC) + + + + + Setup and configuration of Keycloak server + + + + + Setup and configuration of client machines + + + + +
+ Setup of Kerberos server + + This is platform dependent. Exact steps depend on your OS and the Kerberos vendor you're going to use. + Consult Windows Active Directory, MIT Kerberos and your OS documentation for how exactly to setup and configure Kerberos server. + + + At least you will need to: + + + + Add some user principals to your Kerberos database. You can also integrate your Kerberos with LDAP, + which means that user accounts will be provisioned from LDAP server. + + + + + Add service principal for "HTTP" service. For example if your Keycloak server will be running on + www.mydomain.org you may need to add principal HTTP/www.mydomain.org@MYDOMAIN.ORG + assuming that MYDOMAIN.ORG will be your Kerberos realm. + + + For example on MIT Kerberos you can run "kadmin" session. If you are on same machine where is MIT Kerberos, you can simply use command: + + Then add HTTP principal and export his key to keytab file with the commands like: + + Keytab file /tmp/http.keytab will need to be accessible on the host where keycloak server will be running. + + + + +
+
+ Setup and configuration of Keycloak server + + + + Install kerberos client. This is again platform dependent. If you are on Fedora, Ubuntu or RHEL, you can install package freeipa-client, + which contains Kerberos client and bunch of other stuff. + + + + + Configure kerberos client (on linux it's in file /etc/krb5.conf ). You need to put your Kerberos realm and at least + configure the Http domains your server will be running on. For the example realm MYDOMAIN.ORG you may configure domain_realm section like this: + + + + + + Export keytab file with HTTP principal and make sure the file is accessible to the process under which Keycloak + server is running. For production, it's ideal if it's readable just by this process and not by someone else. + For MIT Kerberos example above, we already exported keytab to /tmp/http.keytab . If your KDC and Keycloak + are running on same host, you have file already available. + + + + + Finally run Keycloak server and configure SPNEGO/Kerberos authentication in Keycloak admin console. Keycloak supports Kerberos authentication + through Federation provider SPI . We have 2 federation providers with Kerberos authentication support: + + + Kerberos + + + This provider is useful if you want to authenticate with Kerberos NOT backed by LDAP server. + In this case, users are usually created to Keycloak database after first successful SPNEGO/Kerberos login + and they may need to update profile after first login, as Kerberos protocol itself doesn't provision + any data like first name, last name or email. + + + You can also choose if users can authenticate with classic username/password. In this case, if user doesn't have SPNEGO ticket available, + Keycloak will display login screen and user can fill his Kerberos username and password on login screen. Username/password works also for non-web flows like + Direct Access grants. + + + + + LDAP + + + This provider is useful if you want to authenticate with Kerberos backed by LDAP server. + In this case, data about users are provisioned from LDAP server after successful Kerberos authentication. + + + + + + + +
+
+ Setup and configuration of client machines + + Clients need to install kerberos client and setup krb5.conf as described above. Additionally they need to enable SPNEGO login support in their browser. + See for example this + for more info about Firefox. URI .mydomain.org must be allowed in network.negotiate-auth.trusted-uris config option. + + + In windows domain, clients usually don't need to configure anything special as IE is already able to participate in SPNEGO authentication for the windows domain. + +
+
+ Example setups + + For easier testing with Kerberos, we provided some example setups to test. + +
+ Keycloak and FreeIPA docker image + + Once you install docker, you can run docker image with FreeIPA + server installed. FreeIPA provides integrated security solution with MIT Kerberos and 389 LDAP server among other things . The image provides + also Keycloak server configured with LDAP Federation provider and enabled SPNEGO/Kerberos authentication against the FreeIPA server. + See details here . + +
+
+ ApacheDS testing Kerberos server + + For quick testing and unit tests, we use very simple ApacheDS Kerberos server. + You need to build Keycloak from sources and then run Kerberos server with maven-exec-plugin from our testsuite. See details + here . + +
+
+
+ Troubleshooting + + If you have issues, we recommend to enable more logging by: + + + + Enable Debug flag in admin console for Kerberos or LDAP federation providers + + + + + Enable TRACE logging for category org.keycloak in logging section of $WILDFLY_HOME/standalone/configuration/standalone.xml + to receive more info $WILDFLY_HOME/standalone/log/server.log + + + + + Add system properties -Dsun.security.krb5.debug=true and -Dsun.security.spnego.debug=true + + + + +
+
\ No newline at end of file diff --git a/docbook/reference/en/en-US/modules/user-federation.xml b/docbook/reference/en/en-US/modules/user-federation.xml index 777f4b71fe..a8e6c17358 100755 --- a/docbook/reference/en/en-US/modules/user-federation.xml +++ b/docbook/reference/en/en-US/modules/user-federation.xml @@ -97,6 +97,14 @@ + + Allow Kerberos authentication + + + Enable Kerberos/SPNEGO authentication in realm with users data provisioned from LDAP. More info in Kerberos section. + + + Other options diff --git a/examples/broker/facebook-authentication/facebook-identity-provider-realm.json b/examples/broker/facebook-authentication/facebook-identity-provider-realm.json index 0baa7170bb..5243879de2 100644 --- a/examples/broker/facebook-authentication/facebook-identity-provider-realm.json +++ b/examples/broker/facebook-authentication/facebook-identity-provider-realm.json @@ -39,6 +39,9 @@ "baseUrl": "/facebook-authentication", "redirectUris": [ "/facebook-authentication/*" + ], + "webOrigins": [ + "http://localhost:8080" ] } ], diff --git a/examples/broker/google-authentication/google-identity-provider-realm.json b/examples/broker/google-authentication/google-identity-provider-realm.json index 751bbf064e..22df7e42a3 100644 --- a/examples/broker/google-authentication/google-identity-provider-realm.json +++ b/examples/broker/google-authentication/google-identity-provider-realm.json @@ -39,6 +39,9 @@ "baseUrl": "/google-authentication", "redirectUris": [ "/google-authentication/*" + ], + "webOrigins": [ + "http://localhost:8080" ] } ], diff --git a/examples/broker/saml-broker-authentication/saml-broker-authentication-realm.json b/examples/broker/saml-broker-authentication/saml-broker-authentication-realm.json index 5433cf0eb0..3369dafea0 100644 --- a/examples/broker/saml-broker-authentication/saml-broker-authentication-realm.json +++ b/examples/broker/saml-broker-authentication/saml-broker-authentication-realm.json @@ -38,8 +38,7 @@ "adminUrl": "/saml-broker-authentication", "baseUrl": "/saml-broker-authentication", "redirectUris": [ - "/saml-broker-authentication/*", - "http://localhost:8080/saml-broker-authentication/*" + "/saml-broker-authentication/*" ], "webOrigins": [ "http://localhost:8080" @@ -55,7 +54,7 @@ "updateProfileFirstLogin" : "true", "storeToken" : "true", "config": { - "singleSignOnServiceUrl": "http://localhost:8081/auth/realms/saml-broker-realm/protocol/saml", + "singleSignOnServiceUrl": "http://localhost:8080/auth/realms/saml-broker-realm/protocol/saml", "nameIDPolicyFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "signingCertificate": "MIIDdzCCAl+gAwIBAgIEbySuqTANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE1MDEyODIyMTYyMFoXDTE3MTAyNDIyMTYyMFowbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAII/K9NNvXi9IySl7+l2zY/kKrGTtuR4WdCI0xLW/Jn4dLY7v1/HOnV4CC4ecFOzhdNFPtJkmEhP/q62CpmOYOKApXk3tfmm2rwEz9bWprVxgFGKnbrWlz61Z/cjLAlhD3IUj2ZRBquYgSXQPsYfXo1JmSWF5pZ9uh1FVqu9f4wvRqY20ZhUN+39F+1iaBsoqsrbXypCn1HgZkW1/9D9GZug1c3vB4wg1TwZZWRNGtxwoEhdK6dPrNcZ+6PdanVilWrbQFbBjY4wz8/7IMBzssoQ7Usmo8F1Piv0FGfaVeJqBrcAvbiBMpk8pT+27u6p8VyIX6LhGvnxIwM07NByeSUCAwEAAaMhMB8wHQYDVR0OBBYEFFlcNuTYwI9W0tQ224K1gFJlMam0MA0GCSqGSIb3DQEBCwUAA4IBAQB5snl1KWOJALtAjLqD0mLPg1iElmZP82Lq1htLBt3XagwzU9CaeVeCQ7lTp+DXWzPa9nCLhsC3QyrV3/+oqNli8C6NpeqI8FqN2yQW/QMWN1m5jWDbmrWwtQzRUn/rh5KEb5m3zPB+tOC6e/2bV3QeQebxeW7lVMD0tSCviUg1MQf1l2gzuXQo60411YwqrXwk6GMkDOhFDQKDlMchO3oRbQkGbcP8UeiKAXjMeHfzbiBr+cWz8NYZEtxUEDYDjTpKrYCSMJBXpmgVJCZ00BswbksxJwaGqGMPpUKmCV671pf3m8nq3xyiHMDGuGwtbU+GE8kVx85menmp8+964nin", "wantAuthnRequestsSigned": true, diff --git a/examples/broker/saml-broker-authentication/saml-broker-realm.json b/examples/broker/saml-broker-authentication/saml-broker-realm.json index 0fc06434df..016b843e3a 100644 --- a/examples/broker/saml-broker-authentication/saml-broker-realm.json +++ b/examples/broker/saml-broker-authentication/saml-broker-realm.json @@ -28,10 +28,10 @@ }, "applications": [ { - "name": "http://localhost:8081/auth/", + "name": "http://localhost:8080/auth/", "enabled": true, "redirectUris": [ - "http://localhost:8081/auth/realms/saml-broker-authentication-realm/broker/saml-identity-provider" + "http://localhost:8080/auth/realms/saml-broker-authentication-realm/broker/saml-identity-provider" ], "attributes": { "saml.assertion.signature": "true", diff --git a/examples/broker/saml-broker-authentication/src/main/webapp/keycloak.json b/examples/broker/saml-broker-authentication/src/main/webapp/keycloak.json index dccd4a337f..5c86ef013e 100644 --- a/examples/broker/saml-broker-authentication/src/main/webapp/keycloak.json +++ b/examples/broker/saml-broker-authentication/src/main/webapp/keycloak.json @@ -2,7 +2,7 @@ "realm" : "saml-broker-authentication-realm", "resource" : "saml-broker-authentication", "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", - "auth-server-url": "http://localhost:8081/auth", + "auth-server-url": "/auth", "ssl-required" : "external", "public-client" : true } diff --git a/examples/broker/twitter-authentication/src/main/webapp/keycloak.json b/examples/broker/twitter-authentication/src/main/webapp/keycloak.json new file mode 100644 index 0000000000..7243636390 --- /dev/null +++ b/examples/broker/twitter-authentication/src/main/webapp/keycloak.json @@ -0,0 +1,8 @@ +{ + "realm" : "twitter-identity-provider-realm", + "resource" : "twitter-authentication", + "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url": "/auth", + "ssl-required" : "external", + "public-client" : true +} diff --git a/examples/broker/twitter-authentication/twitter-identity-provider-realm.json b/examples/broker/twitter-authentication/twitter-identity-provider-realm.json index 10c6b55ea0..90aef3fe20 100644 --- a/examples/broker/twitter-authentication/twitter-identity-provider-realm.json +++ b/examples/broker/twitter-authentication/twitter-identity-provider-realm.json @@ -40,6 +40,9 @@ "baseUrl": "/twitter-authentication", "redirectUris": [ "/twitter-authentication/*" + ], + "webOrigins": [ + "http://localhost:8080" ] }, { diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js index 47c3fcac07..59f169fa77 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js @@ -171,7 +171,7 @@ module.config([ '$routeProvider', function($routeProvider) { controller : 'RealmIdentityProviderCtrl' }) .when('/realms/:realm/identity-provider-settings/provider/:provider_id/:id', { - templateUrl : function(params){ return 'partials/realm-identity-provider-' + params.provider_id + '.html'; }, + templateUrl : function(params){ return resourceUrl + '/partials/realm-identity-provider-' + params.provider_id + '.html'; }, resolve : { realm : function(RealmLoader) { return RealmLoader(); diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js index 569c8662b8..b4122885a1 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js @@ -49,75 +49,95 @@ module.controller('ApplicationCredentialsCtrl', function($scope, $location, real }); }); -module.controller('ApplicationIdentityProviderCtrl', function($scope, $location, realm, application, Application, $location, Notifications) { +module.controller('ApplicationIdentityProviderCtrl', function($scope, $location, $route, realm, application, Application, $location, Notifications) { $scope.realm = realm; $scope.application = angular.copy(application); + var length = 0; + + if ($scope.application.identityProviders) { + length = $scope.application.identityProviders.length; + } else { + $scope.application.identityProviders = new Array(realm.identityProviders.length); + } + + for (j = length; j < realm.identityProviders.length; j++) { + $scope.application.identityProviders[j] = {}; + } $scope.identityProviders = []; - if (!$scope.application.allowedIdentityProviders) { - $scope.application.allowedIdentityProviders = []; - } - for (j = 0; j < realm.identityProviders.length; j++) { var identityProvider = realm.identityProviders[j]; var match = false; + var applicationProvider; - for (i = 0; i < $scope.application.allowedIdentityProviders.length; i++) { - var appProvider = $scope.application.allowedIdentityProviders[i]; + for (i = 0; i < $scope.application.identityProviders.length; i++) { + applicationProvider = $scope.application.identityProviders[i]; - if (appProvider == identityProvider.id) { - $scope.identityProviders[i] = identityProvider; - match = true; + if (applicationProvider) { + if (applicationProvider.retrieveToken) { + applicationProvider.retrieveToken = applicationProvider.retrieveToken.toString(); + } else { + applicationProvider.retrieveToken = false.toString(); + } + + if (applicationProvider.id == identityProvider.id) { + $scope.identityProviders[i] = {}; + $scope.identityProviders[i].identityProvider = identityProvider; + $scope.identityProviders[i].retrieveToken = applicationProvider.retrieveToken.toString(); + break; + } + + applicationProvider = null; } } - if (!match) { - var length = $scope.identityProviders.length; + if (applicationProvider == null) { + var length = $scope.identityProviders.length + $scope.application.identityProviders.length; - length = length + $scope.application.allowedIdentityProviders.length; - - $scope.identityProviders[length] = identityProvider; + $scope.identityProviders[length] = {}; + $scope.identityProviders[length].identityProvider = identityProvider; + $scope.identityProviders[length].retrieveToken = false.toString(); } } $scope.identityProviders = $scope.identityProviders.filter(function(n){ return n != undefined }); + var oldCopy = angular.copy($scope.application); + $scope.save = function() { var selectedProviders = []; - for (i = 0; i < $scope.application.allowedIdentityProviders.length; i++) { - var appProvider = $scope.application.allowedIdentityProviders[i]; + for (i = 0; i < $scope.application.identityProviders.length; i++) { + var appProvider = $scope.application.identityProviders[i]; - if (appProvider) { + if (appProvider.id != null && appProvider.id != false) { selectedProviders[selectedProviders.length] = appProvider; } } - $scope.allowedIdentityProviders = $scope.application.allowedIdentityProviders; - $scope.application.allowedIdentityProviders = selectedProviders; + $scope.application.identityProviders = selectedProviders; Application.update({ realm : realm.realm, application : application.id }, $scope.application, function() { $scope.changed = false; - $scope.application.allowedIdentityProviders = $scope.allowedIdentityProviders; - $location.url("/realms/" + realm.realm + "/applications/" + application.id + "/identity-provider"); + $route.reload(); Notifications.success("Your changes have been saved to the application."); }); }; $scope.reset = function() { - $scope.application = angular.copy(application); + $scope.application = angular.copy(oldCopy); $scope.changed = false; }; - $scope.$watch(function() { - return $location.path(); - }, function() { - $scope.path = $location.path().substring(1).split("/"); - }); + $scope.$watch('application', function() { + if (!angular.equals($scope.application, oldCopy)) { + $scope.changed = true; + } + }, true); }); module.controller('ApplicationSamlKeyCtrl', function($scope, $location, $http, $upload, realm, application, diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/oauth-clients.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/oauth-clients.js index dec42f56f3..1237f57301 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/oauth-clients.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/oauth-clients.js @@ -324,74 +324,94 @@ module.controller('OAuthClientRevocationCtrl', function($scope, realm, oauth, OA } }); -module.controller('OAuthClientIdentityProviderCtrl', function($scope, realm, oauth, OAuthClient, $location, Notifications) { +module.controller('OAuthClientIdentityProviderCtrl', function($scope, $route, realm, oauth, OAuthClient, $location, Notifications) { $scope.realm = realm; $scope.oauth = angular.copy(oauth); + var length = 0; + + if ($scope.oauth.identityProviders) { + length = $scope.oauth.identityProviders.length; + } else { + $scope.oauth.identityProviders = new Array(realm.identityProviders.length); + } + + for (j = length; j < realm.identityProviders.length; j++) { + $scope.oauth.identityProviders[j] = {}; + } $scope.identityProviders = []; - if (!$scope.oauth.allowedIdentityProviders) { - $scope.oauth.allowedIdentityProviders = []; - } - for (j = 0; j < realm.identityProviders.length; j++) { var identityProvider = realm.identityProviders[j]; var match = false; + var applicationProvider; - for (i = 0; i < $scope.oauth.allowedIdentityProviders.length; i++) { - var appProvider = $scope.oauth.allowedIdentityProviders[i]; + for (i = 0; i < $scope.oauth.identityProviders.length; i++) { + applicationProvider = $scope.oauth.identityProviders[i]; - if (appProvider == identityProvider.id) { - $scope.identityProviders[i] = identityProvider; - match = true; + if (applicationProvider) { + if (applicationProvider.retrieveToken) { + applicationProvider.retrieveToken = applicationProvider.retrieveToken.toString(); + } else { + applicationProvider.retrieveToken = false.toString(); + } + + if (applicationProvider.id == identityProvider.id) { + $scope.identityProviders[i] = {}; + $scope.identityProviders[i].identityProvider = identityProvider; + $scope.identityProviders[i].retrieveToken = applicationProvider.retrieveToken.toString(); + break; + } + + applicationProvider = null; } } - if (!match) { - var length = $scope.identityProviders.length; + if (applicationProvider == null) { + var length = $scope.identityProviders.length + $scope.oauth.identityProviders.length; - length = length + $scope.oauth.allowedIdentityProviders.length; - - $scope.identityProviders[length] = identityProvider; + $scope.identityProviders[length] = {}; + $scope.identityProviders[length].identityProvider = identityProvider; + $scope.identityProviders[length].retrieveToken = false.toString(); } } $scope.identityProviders = $scope.identityProviders.filter(function(n){ return n != undefined }); + var oldCopy = angular.copy($scope.oauth); + $scope.save = function() { var selectedProviders = []; - for (i = 0; i < $scope.oauth.allowedIdentityProviders.length; i++) { - var appProvider = $scope.oauth.allowedIdentityProviders[i]; + for (i = 0; i < $scope.oauth.identityProviders.length; i++) { + var appProvider = $scope.oauth.identityProviders[i]; - if (appProvider) { + if (appProvider.id != null && appProvider.id != false) { selectedProviders[selectedProviders.length] = appProvider; } } - $scope.allowedIdentityProviders = $scope.oauth.allowedIdentityProviders; - $scope.oauth.allowedIdentityProviders = selectedProviders; + $scope.oauth.identityProviders = selectedProviders; OAuthClient.update({ realm : realm.realm, oauth : oauth.id }, $scope.oauth, function() { $scope.changed = false; - $scope.oauth.allowedIdentityProviders = $scope.allowedIdentityProviders; - $location.url("/realms/" + realm.realm + "/oauth-clients/" + oauth.id + "/identity-provider"); + $route.reload(); Notifications.success("Your changes have been saved to the application."); }); }; $scope.reset = function() { - $scope.oauth = angular.copy(oauth); + $scope.oauth = angular.copy(oldCopy); $scope.changed = false; }; - $scope.$watch(function() { - return $location.path(); - }, function() { - $scope.path = $location.path().substring(1).split("/"); - }); + $scope.$watch('oauth', function() { + if (!angular.equals($scope.oauth, oldCopy)) { + $scope.changed = true; + } + }, true); }); diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-identity-provider.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-identity-provider.html index 57adcd3f35..aeb3b8d961 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-identity-provider.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-identity-provider.html @@ -7,17 +7,24 @@
  • {{application.name}}
  • Identity Provider
  • -

    {{application.name}} Identity Provider Settings

    +

    {{application.name}} Identity Provider Settings

    - {{identityProvider.name}} - + {{identityProvider.identityProvider.name}} +
    - + +
    +
    + +
    + +
    - + +
    diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/oauth-client-identity-provider.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/oauth-client-identity-provider.html index 92f3e87124..619ce331ea 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/oauth-client-identity-provider.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/oauth-client-identity-provider.html @@ -10,14 +10,21 @@

    {{oauth.name}} Identity Provider Settings

    - {{identityProvider.name}} - + {{identityProvider.identityProvider.name}} +
    - + +
    +
    + +
    + +
    - + +
    diff --git a/model/api/src/main/java/org/keycloak/models/ClientIdentityProviderMappingModel.java b/model/api/src/main/java/org/keycloak/models/ClientIdentityProviderMappingModel.java new file mode 100644 index 0000000000..e3b84014a8 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/ClientIdentityProviderMappingModel.java @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source + * + * Copyright 2013 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models; + +/** + * @author pedroigor + */ +public class ClientIdentityProviderMappingModel { + + private String identityProvider; + private boolean retrieveToken; + + public String getIdentityProvider() { + return this.identityProvider; + } + + public void setIdentityProvider(String identityProviderModel) { + this.identityProvider = identityProviderModel; + } + + public boolean isRetrieveToken() { + return this.retrieveToken; + } + + public void setRetrieveToken(boolean retrieveToken) { + this.retrieveToken = retrieveToken; + } +} diff --git a/model/api/src/main/java/org/keycloak/models/ClientModel.java b/model/api/src/main/java/org/keycloak/models/ClientModel.java index 5354d04225..3ebdc2aac7 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java @@ -98,11 +98,10 @@ public interface ClientModel { void setNotBefore(int notBefore); - void updateAllowedIdentityProviders(List identityProviders); - - List getAllowedIdentityProviders(); - + void updateAllowedIdentityProviders(List identityProviders); + List getIdentityProviders(); boolean hasIdentityProvider(String providerId); + boolean isAllowedRetrieveTokenFromIdentityProvider(String providerId); Set getProtocolMappers(); void addProtocolMappers(Set mapperIds); diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java index 3d43feeb3d..134d1bfaf1 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java @@ -29,7 +29,7 @@ public class ClientEntity extends AbstractIdentifiableEntity { private List webOrigins = new ArrayList(); private List redirectUris = new ArrayList(); private List scopeIds = new ArrayList(); - private List allowedIdentityProviders = new ArrayList(); + private List identityProviders = new ArrayList(); private Set protocolMappers = new HashSet(); public String getName() { @@ -144,12 +144,12 @@ public class ClientEntity extends AbstractIdentifiableEntity { this.frontchannelLogout = frontchannelLogout; } - public List getAllowedIdentityProviders() { - return this.allowedIdentityProviders; + public List getIdentityProviders() { + return this.identityProviders; } - public void setAllowedIdentityProviders(List allowedIdentityProviders) { - this.allowedIdentityProviders = allowedIdentityProviders; + public void setIdentityProviders(List identityProviders) { + this.identityProviders = identityProviders; } public Set getProtocolMappers() { diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientIdentityProviderMappingEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientIdentityProviderMappingEntity.java new file mode 100644 index 0000000000..a788aacf4b --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/entities/ClientIdentityProviderMappingEntity.java @@ -0,0 +1,44 @@ +/* + * JBoss, Home of Professional Open Source + * + * Copyright 2013 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.entities; + +/** + * @author pedroigor + */ +public class ClientIdentityProviderMappingEntity { + + private String id; + private Boolean retrieveToken; + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public Boolean isRetrieveToken() { + return this.retrieveToken; + } + + public void setRetrieveToken(Boolean retrieveToken) { + this.retrieveToken = retrieveToken; + } + +} diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 522c96572b..7bd7cfaecc 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -3,6 +3,7 @@ package org.keycloak.models.utils; import org.keycloak.models.ApplicationModel; import org.keycloak.models.ClaimMask; import org.keycloak.models.ClaimTypeModel; +import org.keycloak.models.ClientIdentityProviderMappingModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.FederatedIdentityModel; @@ -19,6 +20,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.ClaimRepresentation; import org.keycloak.representations.idm.ClaimTypeRepresentation; +import org.keycloak.representations.idm.ClientIdentityProviderMappingRepresentation; import org.keycloak.representations.idm.ClientProtocolMappingRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; @@ -262,8 +264,8 @@ public class ModelToRepresentation { rep.setRegisteredNodes(new HashMap(applicationModel.getRegisteredNodes())); } - if (!applicationModel.getAllowedIdentityProviders().isEmpty()) { - rep.setAllowedIdentityProviders(applicationModel.getAllowedIdentityProviders()); + if (!applicationModel.getIdentityProviders().isEmpty()) { + rep.setIdentityProviders(toRepresentation(applicationModel.getIdentityProviders())); } if (!applicationModel.getProtocolMappers().isEmpty()) { @@ -279,6 +281,21 @@ public class ModelToRepresentation { return rep; } + private static List toRepresentation(List identityProviders) { + ArrayList representations = new ArrayList(); + + for (ClientIdentityProviderMappingModel model : identityProviders) { + ClientIdentityProviderMappingRepresentation representation = new ClientIdentityProviderMappingRepresentation(); + + representation.setId(model.getIdentityProvider()); + representation.setRetrieveToken(model.isRetrieveToken()); + + representations.add(representation); + } + + return representations; + } + public static OAuthClientRepresentation toRepresentation(OAuthClientModel model) { OAuthClientRepresentation rep = new OAuthClientRepresentation(); rep.setId(model.getId()); @@ -301,8 +318,8 @@ public class ModelToRepresentation { } rep.setNotBefore(model.getNotBefore()); - if (!model.getAllowedIdentityProviders().isEmpty()) { - rep.setAllowedIdentityProviders(model.getAllowedIdentityProviders()); + if (!model.getIdentityProviders().isEmpty()) { + rep.setIdentityProviders(toRepresentation(model.getIdentityProviders())); } if (!model.getProtocolMappers().isEmpty()) { diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index f296764114..2bf61e2395 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -7,6 +7,7 @@ import org.keycloak.models.ApplicationModel; import org.keycloak.models.BrowserSecurityHeaders; import org.keycloak.models.ClaimMask; import org.keycloak.models.ClaimTypeModel; +import org.keycloak.models.ClientIdentityProviderMappingModel; import org.keycloak.models.ClientModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; @@ -23,6 +24,7 @@ import org.keycloak.models.UserModel; import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.ClaimRepresentation; import org.keycloak.representations.idm.ClaimTypeRepresentation; +import org.keycloak.representations.idm.ClientIdentityProviderMappingRepresentation; import org.keycloak.representations.idm.ClientProtocolMappingRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; @@ -473,17 +475,7 @@ public class RepresentationToModel { applicationModel.setProtocolMappers(ids); } - List allowedIdentityProviders = resourceRep.getAllowedIdentityProviders(); - - if (allowedIdentityProviders == null || allowedIdentityProviders.isEmpty()) { - allowedIdentityProviders = new ArrayList(); - - for (IdentityProviderModel identityProvider : realm.getIdentityProviders()) { - allowedIdentityProviders.add(identityProvider.getId()); - } - } - - applicationModel.updateAllowedIdentityProviders(allowedIdentityProviders); + applicationModel.updateAllowedIdentityProviders(toModel(resourceRep.getIdentityProviders(), realm)); return applicationModel; } @@ -536,9 +528,7 @@ public class RepresentationToModel { setClaims(resource, rep.getClaims()); } - if (rep.getAllowedIdentityProviders() != null) { - resource.updateAllowedIdentityProviders(rep.getAllowedIdentityProviders()); - } + updateClientIdentityProvides(rep.getIdentityProviders(), resource); } public static void setClaims(ClientModel model, ClaimRepresentation rep) { @@ -613,17 +603,7 @@ public class RepresentationToModel { public static OAuthClientModel createOAuthClient(OAuthClientRepresentation rep, RealmModel realm) { OAuthClientModel model = createOAuthClient(rep.getId(), rep.getName(), realm); - List allowedIdentityProviders = rep.getAllowedIdentityProviders(); - - if (allowedIdentityProviders == null || allowedIdentityProviders.isEmpty()) { - allowedIdentityProviders = new ArrayList(); - - for (IdentityProviderModel identityProvider : realm.getIdentityProviders()) { - allowedIdentityProviders.add(identityProvider.getId()); - } - } - - model.updateAllowedIdentityProviders(allowedIdentityProviders); + model.updateAllowedIdentityProviders(toModel(rep.getIdentityProviders(), realm)); updateOAuthClient(rep, model); return model; @@ -667,9 +647,7 @@ public class RepresentationToModel { } } - if (rep.getAllowedIdentityProviders() != null) { - model.updateAllowedIdentityProviders(rep.getAllowedIdentityProviders()); - } + updateClientIdentityProvides(rep.getIdentityProviders(), model); if (rep.getProtocolMappers() != null) { Set ids = new HashSet(); @@ -868,4 +846,48 @@ public class RepresentationToModel { model.setConfig(rep.getConfig()); return model; } + + private static List toModel(List repIdentityProviders, RealmModel realm) { + List allowedIdentityProviders = new ArrayList(); + + if (repIdentityProviders == null || repIdentityProviders.isEmpty()) { + allowedIdentityProviders = new ArrayList(); + + for (IdentityProviderModel identityProvider : realm.getIdentityProviders()) { + ClientIdentityProviderMappingModel identityProviderMapping = new ClientIdentityProviderMappingModel(); + + identityProviderMapping.setIdentityProvider(identityProvider.getId()); + + allowedIdentityProviders.add(identityProviderMapping); + } + } else { + for (ClientIdentityProviderMappingRepresentation rep : repIdentityProviders) { + ClientIdentityProviderMappingModel identityProviderMapping = new ClientIdentityProviderMappingModel(); + + identityProviderMapping.setIdentityProvider(rep.getId()); + identityProviderMapping.setRetrieveToken(rep.isRetrieveToken()); + + allowedIdentityProviders.add(identityProviderMapping); + } + } + + return allowedIdentityProviders; + } + + private static void updateClientIdentityProvides(List identityProviders, ClientModel resource) { + if (identityProviders != null) { + List allowedIdentityProviders = new ArrayList(); + + for (ClientIdentityProviderMappingRepresentation mappingRepresentation : identityProviders) { + ClientIdentityProviderMappingModel identityProviderMapping = new ClientIdentityProviderMappingModel(); + + identityProviderMapping.setIdentityProvider(mappingRepresentation.getId()); + identityProviderMapping.setRetrieveToken(mappingRepresentation.isRetrieveToken()); + + allowedIdentityProviders.add(identityProviderMapping); + } + + resource.updateAllowedIdentityProviders(allowedIdentityProviders); + } + } } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ClientAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ClientAdapter.java index 0fc38bcc9d..a17cc7c14a 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ClientAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ClientAdapter.java @@ -1,5 +1,6 @@ package org.keycloak.models.cache; +import org.keycloak.models.ClientIdentityProviderMappingModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; @@ -263,15 +264,15 @@ public abstract class ClientAdapter implements ClientModel { } @Override - public void updateAllowedIdentityProviders(List identityProviders) { + public void updateAllowedIdentityProviders(List identityProviders) { getDelegateForUpdate(); updatedClient.updateAllowedIdentityProviders(identityProviders); } @Override - public List getAllowedIdentityProviders() { - if (updatedClient != null) return updatedClient.getAllowedIdentityProviders(); - return cachedClient.getAllowedIdentityProviders(); + public List getIdentityProviders() { + if (updatedClient != null) return updatedClient.getIdentityProviders(); + return cachedClient.getIdentityProviders(); } @Override @@ -280,6 +281,12 @@ public abstract class ClientAdapter implements ClientModel { return cachedClient.hasIdentityProvider(providerId); } + @Override + public boolean isAllowedRetrieveTokenFromIdentityProvider(String providerId) { + if (updatedClient != null) return updatedClient.isAllowedRetrieveTokenFromIdentityProvider(providerId); + return cachedClient.isAllowedRetrieveTokenFromIdentityProvider(providerId); + } + @Override public Set getProtocolMappers() { if (updatedClient != null) return updatedClient.getProtocolMappers(); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java index 1029713ba6..e9a8a89b74 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java @@ -1,5 +1,6 @@ package org.keycloak.models.cache.entities; +import org.keycloak.models.ClientIdentityProviderMappingModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; @@ -35,7 +36,7 @@ public class CachedClient { protected int notBefore; protected Set scope = new HashSet(); protected Set webOrigins = new HashSet(); - private List allowedIdentityProviders = new ArrayList(); + private List identityProviders = new ArrayList(); private Set protocolClaimMappings = new HashSet(); public CachedClient(RealmCache cache, RealmProvider delegate, RealmModel realm, ClientModel model) { @@ -57,7 +58,7 @@ public class CachedClient { for (RoleModel role : model.getScopeMappings()) { scope.add(role.getId()); } - this.allowedIdentityProviders = model.getAllowedIdentityProviders(); + this.identityProviders = model.getIdentityProviders(); protocolClaimMappings.addAll(model.getProtocolMappers()); } @@ -125,15 +126,31 @@ public class CachedClient { return frontchannelLogout; } - public List getAllowedIdentityProviders() { - return this.allowedIdentityProviders; + public List getIdentityProviders() { + return this.identityProviders; } public boolean hasIdentityProvider(String providerId) { - return this.allowedIdentityProviders.contains(providerId); + for (ClientIdentityProviderMappingModel model : getIdentityProviders()) { + if (model.getIdentityProvider().equals(providerId)) { + return true; + } + } + + return false; } public Set getProtocolClaimMappings() { return protocolClaimMappings; } + + public boolean isAllowedRetrieveTokenFromIdentityProvider(String providerId) { + for (ClientIdentityProviderMappingModel model : getIdentityProviders()) { + if (model.getIdentityProvider().equals(providerId)) { + return model.isRetrieveToken(); + } + } + + return false; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java index c437014cbc..04af5ee4a4 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java @@ -1,14 +1,15 @@ package org.keycloak.models.jpa; +import org.keycloak.models.ClientIdentityProviderMappingModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; import org.keycloak.models.jpa.entities.ClientEntity; +import org.keycloak.models.jpa.entities.ClientIdentityProviderMappingEntity; import org.keycloak.models.jpa.entities.IdentityProviderEntity; import org.keycloak.models.jpa.entities.ProtocolMapperEntity; -import org.keycloak.models.jpa.entities.RealmEntity; import org.keycloak.models.jpa.entities.RoleEntity; import org.keycloak.models.jpa.entities.ScopeMappingEntity; @@ -303,47 +304,96 @@ public abstract class ClientAdapter implements ClientModel { } @Override - public void updateAllowedIdentityProviders(List identityProviders) { - Collection entities = entity.getAllowedIdentityProviders(); + public void updateAllowedIdentityProviders(List identityProviders) { + Collection entities = entity.getIdentityProviders(); Set already = new HashSet(); - List remove = new ArrayList(); - for (IdentityProviderEntity rel : entities) { - if (!contains(rel.getId(), identityProviders.toArray(new String[identityProviders.size()]))) { - remove.add(rel); + List remove = new ArrayList(); + + for (ClientIdentityProviderMappingEntity entity : entities) { + IdentityProviderEntity identityProvider = entity.getIdentityProvider(); + boolean toRemove = true; + + for (ClientIdentityProviderMappingModel model : identityProviders) { + if (model.getIdentityProvider().equals(identityProvider.getId())) { + toRemove = false; + break; + } + } + + if (toRemove) { + remove.add(entity); } else { - already.add(rel.getId()); + already.add(entity.getIdentityProvider().getId()); } } - for (IdentityProviderEntity entity : remove) { + for (ClientIdentityProviderMappingEntity entity : remove) { entities.remove(entity); + em.remove(entity); } em.flush(); - for (String providerId : identityProviders) { - if (!already.contains(providerId)) { - TypedQuery query = em.createNamedQuery("findIdentityProviderById", IdentityProviderEntity.class).setParameter("id", providerId); - IdentityProviderEntity providerEntity = query.getSingleResult(); - entities.add(providerEntity); + for (ClientIdentityProviderMappingModel model : identityProviders) { + ClientIdentityProviderMappingEntity mappingEntity = null; + + if (!already.contains(model.getIdentityProvider())) { + mappingEntity = new ClientIdentityProviderMappingEntity(); + entities.add(mappingEntity); + } else { + for (ClientIdentityProviderMappingEntity entity : entities) { + if (entity.getIdentityProvider().getId().equals(model.getIdentityProvider())) { + mappingEntity = entity; + break; + } + } } + + TypedQuery query = em.createNamedQuery("findIdentityProviderById", IdentityProviderEntity.class).setParameter("id", model.getIdentityProvider()); + IdentityProviderEntity identityProviderEntity = query.getSingleResult(); + + mappingEntity.setIdentityProvider(identityProviderEntity); + mappingEntity.setClient(this.entity); + mappingEntity.setRetrieveToken(model.isRetrieveToken()); + + em.persist(mappingEntity); } em.flush(); } @Override - public List getAllowedIdentityProviders() { - Collection entities = entity.getAllowedIdentityProviders(); - List providers = new ArrayList(); + public List getIdentityProviders() { + List models = new ArrayList(); - for (IdentityProviderEntity entity : entities) { - providers.add(entity.getId()); + for (ClientIdentityProviderMappingEntity entity : this.entity.getIdentityProviders()) { + ClientIdentityProviderMappingModel model = new ClientIdentityProviderMappingModel(); + + model.setIdentityProvider(entity.getIdentityProvider().getId()); + model.setRetrieveToken(entity.isRetrieveToken()); + + models.add(model); } - return providers; + return models; } @Override public boolean hasIdentityProvider(String providerId) { - List allowedIdentityProviders = getAllowedIdentityProviders(); - return allowedIdentityProviders.contains(providerId); + for (ClientIdentityProviderMappingModel model : getIdentityProviders()) { + if (model.getIdentityProvider().equals(providerId)) { + return true; + } + } + + return false; + } + + @Override + public boolean isAllowedRetrieveTokenFromIdentityProvider(String providerId) { + for (ClientIdentityProviderMappingModel model : getIdentityProviders()) { + if (model.getIdentityProvider().equals(providerId)) { + return model.isRetrieveToken(); + } + } + + return false; } public static boolean contains(String str, String[] array) { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java index 817fd8085c..dcf33cb769 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java @@ -73,9 +73,8 @@ public abstract class ClientEntity { @CollectionTable(name="CLIENT_ATTRIBUTES", joinColumns={ @JoinColumn(name="CLIENT_ID") }) protected Map attributes = new HashMap(); - @OneToMany(fetch = FetchType.LAZY) - @JoinTable(name="CLIENT_ALLOWED_IDENTITY_PROVIDER", joinColumns = { @JoinColumn(name="CLIENT_ID")}, inverseJoinColumns = { @JoinColumn(name="INTERNAL_ID")}) - Collection allowedIdentityProviders = new ArrayList(); + @OneToMany(fetch = FetchType.LAZY, mappedBy = "client", cascade = CascadeType.REMOVE) + Collection identityProviders = new ArrayList(); @OneToMany(fetch = FetchType.LAZY) @JoinTable(name="CLIENT_PROTOCOL_MAPPER", joinColumns = { @JoinColumn(name="CLIENT_ID")}, inverseJoinColumns = { @JoinColumn(name="MAPPING_ID")}) @@ -193,12 +192,12 @@ public abstract class ClientEntity { this.frontchannelLogout = frontchannelLogout; } - public Collection getAllowedIdentityProviders() { - return this.allowedIdentityProviders; + public Collection getIdentityProviders() { + return this.identityProviders; } - public void setAllowedIdentityProviders(Collection allowedIdentityProviders) { - this.allowedIdentityProviders = allowedIdentityProviders; + public void setIdentityProviders(Collection identityProviders) { + this.identityProviders = identityProviders; } public Collection getProtocolMappers() { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientIdentityProviderMappingEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientIdentityProviderMappingEntity.java new file mode 100755 index 0000000000..fe8485d608 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientIdentityProviderMappingEntity.java @@ -0,0 +1,121 @@ +package org.keycloak.models.jpa.entities; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import java.io.Serializable; + +/** + * @author pedroigor + */ +@Table(name="CLIENT_IDENTITY_PROVIDER_MAPPING") +@Entity +@IdClass(ClientIdentityProviderMappingEntity.Key.class) +public class ClientIdentityProviderMappingEntity { + + @Id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "CLIENT_ID") + private ClientEntity client; + + @Id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "IDENTITY_PROVIDER_ID") + private IdentityProviderEntity identityProvider; + + @Column(name = "RETRIEVE_TOKEN") + private boolean retrieveToken; + + public ClientEntity getClient() { + return this.client; + } + + public void setClient(ClientEntity client) { + this.client = client; + } + + public IdentityProviderEntity getIdentityProvider() { + return this.identityProvider; + } + + public void setIdentityProvider(IdentityProviderEntity identityProvider) { + this.identityProvider = identityProvider; + } + + public void setRetrieveToken(boolean retrieveToken) { + this.retrieveToken = retrieveToken; + } + + public boolean isRetrieveToken() { + return retrieveToken; + } + + public static class Key implements Serializable { + + private ClientEntity client; + private IdentityProviderEntity identityProvider; + + public Key() { + } + + public Key(ClientEntity client, IdentityProviderEntity identityProvider) { + this.client = client; + this.identityProvider = identityProvider; + } + + public ClientEntity getUser() { + return client; + } + + public IdentityProviderEntity getIdentityProvider() { + return identityProvider; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Key key = (Key) o; + + if (identityProvider != null ? !identityProvider.getId().equals(key.identityProvider.getId()) : key.identityProvider != null) + return false; + if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = client != null ? client.getId().hashCode() : 0; + result = 31 * result + (identityProvider != null ? identityProvider.hashCode() : 0); + return result; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ClientIdentityProviderMappingEntity key = (ClientIdentityProviderMappingEntity) o; + + if (identityProvider != null ? !identityProvider.getId().equals(key.identityProvider.getId()) : key.identityProvider != null) + return false; + if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = client != null ? client.getId().hashCode() : 0; + result = 31 * result + (identityProvider != null ? identityProvider.hashCode() : 0); + return result; + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java index 3c472821f6..c0202c1f52 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java @@ -2,6 +2,7 @@ package org.keycloak.models.mongo.keycloak.adapters; import org.keycloak.connections.mongo.api.MongoIdentifiableEntity; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.ClientIdentityProviderMappingModel; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; @@ -9,6 +10,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RealmProvider; import org.keycloak.models.RoleModel; import org.keycloak.models.entities.ClientEntity; +import org.keycloak.models.entities.ClientIdentityProviderMappingEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; import org.keycloak.models.mongo.utils.MongoModelUtils; @@ -323,24 +325,58 @@ public abstract class ClientAdapter extends A } @Override - public void updateAllowedIdentityProviders(List identityProviders) { - List providerIds = new ArrayList(); - for (String providerId : identityProviders) { - providerIds.add(providerId); + public void updateAllowedIdentityProviders(List identityProviders) { + List stored = getMongoEntityAsClient().getIdentityProviders(); + + for (ClientIdentityProviderMappingModel model : identityProviders) { + ClientIdentityProviderMappingEntity entity = new ClientIdentityProviderMappingEntity(); + + entity.setId(model.getIdentityProvider()); + entity.setRetrieveToken(model.isRetrieveToken()); } - getMongoEntityAsClient().setAllowedIdentityProviders(identityProviders); + getMongoEntityAsClient().setIdentityProviders(stored); updateMongoEntity(); } @Override - public List getAllowedIdentityProviders() { - return getMongoEntityAsClient().getAllowedIdentityProviders(); + public List getIdentityProviders() { + List models = new ArrayList(); + + for (ClientIdentityProviderMappingEntity entity : getMongoEntityAsClient().getIdentityProviders()) { + ClientIdentityProviderMappingModel model = new ClientIdentityProviderMappingModel(); + + model.setIdentityProvider(entity.getId()); + model.setRetrieveToken(entity.isRetrieveToken()); + + models.add(model); + } + + return models; } @Override public boolean hasIdentityProvider(String providerId) { - List allowedIdentityProviders = getMongoEntityAsClient().getAllowedIdentityProviders(); - return allowedIdentityProviders.contains(providerId); + for (ClientIdentityProviderMappingEntity identityProviderMappingModel : getMongoEntityAsClient().getIdentityProviders()) { + String identityProvider = identityProviderMappingModel.getId(); + + if (identityProvider.equals(providerId)) { + return true; + } + } + + return false; } + + @Override + public boolean isAllowedRetrieveTokenFromIdentityProvider(String providerId) { + for (ClientIdentityProviderMappingEntity identityProviderMappingModel : getMongoEntityAsClient().getIdentityProviders()) { + if (identityProviderMappingModel.getId().equals(providerId)) { + return identityProviderMappingModel.isRetrieveToken(); + } + } + + return false; + } + } diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index 5de4015ddb..7fb4209ca4 100644 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -186,6 +186,10 @@ public class IdentityBrokerService { return corsResponse(badRequest("Client [" + audience + "] not authorized."), clientModel); } + if (!clientModel.isAllowedRetrieveTokenFromIdentityProvider(providerId)) { + return corsResponse(badRequest("Client [" + audience + "] not authorized to retrieve tokens from identity provider [" + providerId + "]."), clientModel); + } + if (OAuthClientModel.class.isInstance(clientModel) && !forceRetrieval) { return corsResponse(Flows.forms(this.session, this.realmModel, clientModel, this.uriInfo) .setClientSessionCode(authManager.extractAuthorizationHeaderToken(this.request.getHttpHeaders())) @@ -323,7 +327,7 @@ public class IdentityBrokerService { federatedUser.addRequiredAction(UPDATE_PROFILE); } } catch (Exception e) { - return redirectToLoginPage(e.getMessage(), clientCode); + return redirectToLoginPage(e, clientCode); } } @@ -439,16 +443,21 @@ public class IdentityBrokerService { } private Response redirectToErrorPage(String message, Throwable throwable) { + if (message == null) { + message = "Unexpected error when authenticating with identity provider"; + } + fireErrorEvent(message, throwable); return Flows.forwardToSecurityFailurePage(this.session, this.realmModel, this.uriInfo, message); } - private Response badRequest(String message) { - fireErrorEvent(message); - return Flows.errors().error(message, Status.BAD_REQUEST); - } + private Response redirectToLoginPage(Throwable t, ClientSessionCode clientCode) { + String message = t.getMessage(); + + if (message == null) { + message = "Unexpected error when authenticating with identity provider"; + } - private Response redirectToLoginPage(String message, ClientSessionCode clientCode) { fireErrorEvent(message); return Flows.forms(this.session, this.realmModel, clientCode.getClientSession().getClient(), this.uriInfo) .setClientSessionCode(clientCode.getCode()) @@ -456,6 +465,11 @@ public class IdentityBrokerService { .createLogin(); } + private Response badRequest(String message) { + fireErrorEvent(message); + return Flows.errors().error(message, Status.BAD_REQUEST); + } + private IdentityProvider getIdentityProvider(String providerId) { IdentityProviderModel identityProviderModel = this.realmModel.getIdentityProviderById(providerId); @@ -513,7 +527,11 @@ public class IdentityBrokerService { FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(updatedIdentity.getIdentityProviderId(), updatedIdentity.getId(), updatedIdentity.getUsername(), updatedIdentity.getToken()); // Check if no user already exists with this username or email - UserModel existingUser = this.session.users().getUserByEmail(updatedIdentity.getEmail(), this.realmModel); + UserModel existingUser = null; + + if (updatedIdentity.getEmail() != null) { + existingUser = this.session.users().getUserByEmail(updatedIdentity.getEmail(), this.realmModel); + } if (existingUser != null) { fireErrorEvent(Errors.FEDERATED_IDENTITY_EMAIL_EXISTS); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java index e8b184edce..bac9b03027 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java @@ -1,6 +1,7 @@ package org.keycloak.services.resources.admin; import org.jboss.resteasy.annotations.cache.NoCache; +import org.keycloak.models.ClientIdentityProviderMappingModel; import org.keycloak.models.ClientModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; @@ -17,6 +18,7 @@ import javax.ws.rs.GET; import javax.ws.rs.PUT; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; +import java.util.ArrayList; import java.util.List; /** @@ -63,11 +65,15 @@ public class IdentityProviderResource { private void removeClientIdentityProviders(List clients, IdentityProviderModel identityProvider) { for (ClientModel clientModel : clients) { - List allowedIdentityProviders = clientModel.getAllowedIdentityProviders(); + List identityProviders = clientModel.getIdentityProviders(); - allowedIdentityProviders.remove(identityProvider.getId()); + for (ClientIdentityProviderMappingModel providerMappingModel : new ArrayList(identityProviders)) { + if (providerMappingModel.getIdentityProvider().equals(identityProvider.getId())) { + identityProviders.remove(providerMappingModel); + } + } - clientModel.updateAllowedIdentityProviders(allowedIdentityProviders); + clientModel.updateAllowedIdentityProviders(identityProviders); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java index d44e464c44..d7d1a679b3 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java @@ -7,6 +7,7 @@ import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; +import org.keycloak.models.ClientIdentityProviderMappingModel; import org.keycloak.models.ClientModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; @@ -179,9 +180,12 @@ public class IdentityProvidersResource { private void updateClientIdentityProviders(List clients, IdentityProviderRepresentation identityProvider) { for (ClientModel clientModel : clients) { - List allowedIdentityProviders = clientModel.getAllowedIdentityProviders(); + List allowedIdentityProviders = clientModel.getIdentityProviders(); + ClientIdentityProviderMappingModel providerMappingModel = new ClientIdentityProviderMappingModel(); - allowedIdentityProviders.add(identityProvider.getId()); + providerMappingModel.setIdentityProvider(identityProvider.getId()); + + allowedIdentityProviders.add(providerMappingModel); clientModel.updateAllowedIdentityProviders(allowedIdentityProviders); } diff --git a/testsuite/integration/README.md b/testsuite/integration/README.md index a8d06711ce..f76392ed54 100644 --- a/testsuite/integration/README.md +++ b/testsuite/integration/README.md @@ -100,12 +100,12 @@ To start a ApacheDS based LDAP server for testing LDAP sending run: There are additional system properties you can use to configure (See EmbeddedServersFactory class for details). Once done, you can create LDAP Federation provider in Keycloak admin console with the settings like: -Vendor: Other -Connection URL: ldap://localhost:10389 -Base DN: dc=keycloak,dc=org -User DN Suffix: ou=People,dc=keycloak,dc=org -Bind DN: uid=admin,ou=system -Bind credential: secret +* Vendor: Other +* Connection URL: ldap://localhost:10389 +* Base DN: dc=keycloak,dc=org +* User DN Suffix: ou=People,dc=keycloak,dc=org +* Bind DN: uid=admin,ou=system +* Bind credential: secret Kerberos server --------------- @@ -114,10 +114,40 @@ To start a ApacheDS based Kerberos server for testing Kerberos + LDAP sending ru mvn exec:java -Pkerberos -There are additional system properties you can use to configure (See EmbeddedServersFactory class for details). Once done, you can create LDAP Federation provider -in Keycloak admin console with same settings like mentioned in previous LDAP section. And you can enable Kerberos with the settings like: +There are additional system properties you can use to configure (See EmbeddedServersFactory class for details) but for testing purposes default values should be good. +By default ApacheDS LDAP server will be running on localhost:10389 and Kerberos KDC on localhost:6088 . LDAP will import initial data from [src/main/resources/kerberos/users-kerberos.ldif](src/main/resources/kerberos/users-kerberos.ldif) . -Server Principal: HTTP/localhost@KEYCLOAK.ORG -KeyTab: $KEYCLOAK_SOURCES/testsuite/integration/src/main/resources/kerberos/http.keytab +Once kerberos is running, you can create LDAP Federation provider in Keycloak admin console with same settings like mentioned in previous LDAP section. +But additionally you can enable Kerberos authentication in LDAP provider with the settings like: + +* Kerberos realm: KEYCLOAK.ORG +* Server Principal: HTTP/localhost@KEYCLOAK.ORG +* KeyTab: $KEYCLOAK_SOURCES/testsuite/integration/src/main/resources/kerberos/http.keytab (Replace $KEYCLOAK_SOURCES with correct absolute path of your sources) + +Once you do this, you should also ensure that your Kerberos client configuration file is properly configured with KEYCLOAK.ORG domain. +See [src/main/resources/kerberos/test-krb5.conf](src/main/resources/kerberos/test-krb5.conf) for inspiration. The location of Kerberos configuration file +is platform dependent (In linux it's file `/etc/krb5.conf` ) + +Then you need to configure your browser to allow SPNEGO/Kerberos login from `localhost` . + +Exact steps are again browser dependent. For Firefox see for example [http://www.microhowto.info/howto/configure_firefox_to_authenticate_using_spnego_and_kerberos.html](http://www.microhowto.info/howto/configure_firefox_to_authenticate_using_spnego_and_kerberos.html) . +URI `localhost` must be allowed in `network.negotiate-auth.trusted-uris` config option. + +For Chrome, you just need to run the browser with command similar to this (more details in Chrome documentation): + +``` +/usr/bin/google-chrome-stable --auth-server-whitelist="localhost" +``` + + +Finally test the integration by retrieve kerberos ticket. In many OS you can achieve this by running command from CMD like: + +``` +kinit hnelson@KEYCLOAK.ORG +``` + +and provide password `secret` + +Now when you access `http://localhost:8081/auth/realms/master/account` you should be logged in automatically as user `hnelson` . diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/KerberosEmbeddedServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/KerberosEmbeddedServer.java index ff5b7791e1..527c9b3807 100644 --- a/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/KerberosEmbeddedServer.java +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/KerberosEmbeddedServer.java @@ -54,7 +54,7 @@ public class KerberosEmbeddedServer extends LDAPEmbeddedServer { public void init() throws Exception { super.init(); - log.info("Creating KDC server"); + log.info("Creating KDC server. kerberosRealm: " + kerberosRealm + ", kdcPort: " + kdcPort); createAndStartKdcServer(); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java index 6921ec3132..dd034c1e82 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -25,6 +25,8 @@ import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.models.ApplicationModel; +import org.keycloak.models.ClientIdentityProviderMappingModel; +import org.keycloak.models.ClientModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; @@ -156,11 +158,18 @@ public abstract class AbstractIdentityProviderTest { IdentityProviderModel identityProviderModel = getIdentityProviderModel(); RealmModel realm = getRealm(); ApplicationModel applicationModel = realm.getApplicationByName("test-app"); - List allowedIdentityProviders = applicationModel.getAllowedIdentityProviders(); + List allowedIdentityProviders = applicationModel.getIdentityProviders(); + ClientIdentityProviderMappingModel mapping = null; - assertTrue(allowedIdentityProviders.contains(identityProviderModel.getId())); + for (ClientIdentityProviderMappingModel model : allowedIdentityProviders) { + if (model.getIdentityProvider().equals(identityProviderModel.getId())) { + mapping = model; + } + } - allowedIdentityProviders.remove(identityProviderModel.getId()); + assertNotNull(mapping); + + allowedIdentityProviders.remove(mapping); this.driver.navigate().to("http://localhost:8081/test-app/"); @@ -173,7 +182,7 @@ public abstract class AbstractIdentityProviderTest { } - allowedIdentityProviders.add(identityProviderModel.getId()); + allowedIdentityProviders.add(mapping); applicationModel.updateAllowedIdentityProviders(allowedIdentityProviders); @@ -317,6 +326,18 @@ public abstract class AbstractIdentityProviderTest { assertNotNull(identityModel.getToken()); + ClientModel clientModel = realm.findClient("test-app"); + ClientIdentityProviderMappingModel providerMappingModel = null; + + for (ClientIdentityProviderMappingModel identityProviderMappingModel : clientModel.getIdentityProviders()) { + if (identityProviderMappingModel.getIdentityProvider().equals(getProviderId())) { + providerMappingModel = identityProviderMappingModel; + break; + } + } + + providerMappingModel.setRetrieveToken(false); + UserSessionStatus userSessionStatus = retrieveSessionStatus(); String accessToken = userSessionStatus.getAccessTokenString(); URI tokenEndpointUrl = Urls.identityProviderRetrieveToken(BASE_URI, getProviderId(), realm.getName()); @@ -331,6 +352,14 @@ public abstract class AbstractIdentityProviderTest { WebTarget tokenEndpoint = client.target(tokenEndpointUrl); Response response = tokenEndpoint.request().get(); + assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + + providerMappingModel.setRetrieveToken(true); + + client = ClientBuilder.newBuilder().register(authFilter).build(); + tokenEndpoint = client.target(tokenEndpointUrl); + response = tokenEndpoint.request().get(); + assertEquals(Status.OK.getStatusCode(), response.getStatus()); assertNotNull(response.readEntity(String.class)); @@ -375,6 +404,18 @@ public abstract class AbstractIdentityProviderTest { assertTrue(oauth.getCurrentQuery().containsKey(OAuth2Constants.CODE)); + ClientModel clientModel = getRealm().findClient("third-party"); + ClientIdentityProviderMappingModel providerMappingModel = null; + + for (ClientIdentityProviderMappingModel identityProviderMappingModel : clientModel.getIdentityProviders()) { + if (identityProviderMappingModel.getIdentityProvider().equals(getProviderId())) { + providerMappingModel = identityProviderMappingModel; + break; + } + } + + providerMappingModel.setRetrieveToken(true); + AccessTokenResponse accessToken = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password"); URI tokenEndpointUrl = Urls.identityProviderRetrieveToken(BASE_URI, getProviderId(), getRealm().getName()); String authHeader = "Bearer " + accessToken.getAccessToken(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java index d654d1e5d9..8f43860e05 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java @@ -25,6 +25,8 @@ import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; import org.keycloak.broker.saml.SAMLIdentityProvider; import org.keycloak.broker.saml.SAMLIdentityProviderConfig; import org.keycloak.broker.saml.SAMLIdentityProviderFactory; +import org.keycloak.models.ClientIdentityProviderMappingModel; +import org.keycloak.models.ClientModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.RealmModel; import org.keycloak.representations.idm.RealmRepresentation; @@ -113,6 +115,31 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes assertFalse(identityProviderModel.isAuthenticateByDefault()); } + @Test + public void testApplicationIdentityProviders() throws Exception { + RealmModel realm = installTestRealm(); + + ClientModel client = realm.findClient("test-app-with-allowed-providers"); + List identityProviders = client.getIdentityProviders(); + + assertEquals(1, identityProviders.size()); + + ClientIdentityProviderMappingModel identityProviderMappingModel = identityProviders.get(0); + + assertEquals("kc-oidc-idp", identityProviderMappingModel.getIdentityProvider()); + assertEquals(false, identityProviderMappingModel.isRetrieveToken()); + + identityProviders.remove(identityProviderMappingModel); + + client.updateAllowedIdentityProviders(identityProviders); + + client = realm.findClientById(client.getId()); + identityProviders = client.getIdentityProviders(); + + assertEquals(0, identityProviders.size()); + } + + private void assertIdentityProviderConfig(List identityProviders) { assertFalse(identityProviders.isEmpty()); diff --git a/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json b/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json index 322cebe423..c567c177df 100755 --- a/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json +++ b/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json @@ -197,8 +197,11 @@ "/test-app/*" ], "webOrigins": [], - "allowedIdentityProviders": [ - "kc-oidc-idp" + "identityProviders": [ + { + "id": "kc-oidc-idp", + "retrieveToken": false + } ] } ],