From 4b1ba704ec88c1107d3bac14035dce572b6c7458 Mon Sep 17 00:00:00 2001 From: pedroigor Date: Fri, 6 Feb 2015 22:23:34 -0200 Subject: [PATCH] [KEYCLOAK-992] - Token retrieval from brokered idps. --- .../broker/provider/FederatedIdentity.java | 8 + .../broker/provider/IdentityProvider.java | 5 + .../oidc/AbstractOAuth2IdentityProvider.java | 51 +++-- .../broker/oidc/OIDCIdentityProvider.java | 9 +- .../broker/saml/SAMLIdentityProvider.java | 25 ++- .../META-INF/jpa-changelog-1.2.0.Beta1.xml | 13 ++ .../idm/ApplicationRepresentation.java | 9 + .../idm/IdentityProviderRepresentation.java | 25 ++- .../idm/OAuthClientRepresentation.java | 9 + .../theme/admin/base/resources/js/app.js | 66 ++++++ .../resources/js/controllers/applications.js | 71 +++++++ .../resources/js/controllers/oauth-clients.js | 70 ++++++ .../application-identity-provider.html | 24 +++ .../oauth-client-identity-provider.html | 24 +++ .../realm-identity-provider-oidc.html | 7 + .../realm-identity-provider-saml.html | 7 + .../realm-identity-provider-social.html | 7 + .../templates/kc-navigation-application.html | 1 + .../templates/kc-navigation-oauth-client.html | 1 + .../theme/login/base/login-oauth-grant.ftl | 30 ++- .../keycloak/login/LoginFormsProvider.java | 3 + .../FreeMarkerLoginFormsProvider.java | 20 +- .../model/IdentityProviderBean.java | 15 +- .../freemarker/model/OAuthGrantBean.java | 8 +- .../login/freemarker/model/UrlBean.java | 9 +- .../org/keycloak/models/ApplicationModel.java | 1 - .../java/org/keycloak/models/ClientModel.java | 6 + .../models/FederatedIdentityModel.java | 20 +- .../models/IdentityProviderModel.java | 11 + .../models/entities/ClientEntity.java | 9 + .../entities/FederatedIdentityEntity.java | 9 + .../entities/IdentityProviderEntity.java | 14 +- .../models/utils/ModelToRepresentation.java | 9 + .../models/utils/RepresentationToModel.java | 42 ++-- .../keycloak/models/cache/ClientAdapter.java | 19 ++ .../models/cache/entities/CachedClient.java | 17 +- .../models/jpa/ApplicationAdapter.java | 9 +- .../keycloak/models/jpa/ClientAdapter.java | 59 ++++++ .../keycloak/models/jpa/JpaUserProvider.java | 5 +- .../org/keycloak/models/jpa/RealmAdapter.java | 2 + .../models/jpa/entities/ClientEntity.java | 16 ++ .../jpa/entities/FederatedIdentityEntity.java | 11 + .../jpa/entities/IdentityProviderEntity.java | 16 ++ .../keycloak/adapters/ApplicationAdapter.java | 1 - .../keycloak/adapters/ClientAdapter.java | 25 +++ .../keycloak/adapters/MongoUserProvider.java | 13 +- .../mongo/keycloak/adapters/RealmAdapter.java | 2 + .../AuthenticationBrokerResource.java | 135 ++++++++++-- .../admin/IdentityProviderResource.java | 25 +++ .../facebook/FacebookIdentityProvider.java | 3 +- .../social/github/GitHubIdentityProvider.java | 2 +- .../twitter/TwitterIdentityProvider.java | 7 + .../testsuite/account/AccountTest.java | 3 +- .../broker/AbstractIdentityProviderTest.java | 199 ++++++++++++++---- .../testsuite/broker/BrokerKeyCloakRule.java | 1 + .../broker/ImportIdentityProviderTest.java | 6 +- .../OIDCKeyCloakServerBrokerBasicTest.java | 19 ++ .../SAMLKeyCloakServerBrokerBasicTest.java | 28 ++- ...KeyCloakServerBrokerWithSignatureTest.java | 28 ++- .../provider/CustomIdentityProvider.java | 8 + .../provider/social/CustomSocialProvider.java | 8 + .../broker/util/UserSessionStatusServlet.java | 22 +- .../testsuite/pages/AccountPasswordPage.java | 20 +- .../broker-test/test-realm-with-broker.json | 16 +- 64 files changed, 1193 insertions(+), 170 deletions(-) create mode 100755 forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-identity-provider.html create mode 100755 forms/common-themes/src/main/resources/theme/admin/base/resources/partials/oauth-client-identity-provider.html diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/FederatedIdentity.java b/broker/core/src/main/java/org/keycloak/broker/provider/FederatedIdentity.java index ac853f7e3e..8f81a6f0d8 100644 --- a/broker/core/src/main/java/org/keycloak/broker/provider/FederatedIdentity.java +++ b/broker/core/src/main/java/org/keycloak/broker/provider/FederatedIdentity.java @@ -30,6 +30,7 @@ public class FederatedIdentity { private String firstName; private String lastName; private String email; + private String token; public FederatedIdentity(String id) { if (id == null) { @@ -84,4 +85,11 @@ public class FederatedIdentity { } + public void setToken(String token) { + this.token = token; + } + + public String getToken() { + return this.token; + } } diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java index cfe6f41dc7..f84765af91 100644 --- a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java +++ b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java @@ -17,9 +17,12 @@ */ package org.keycloak.broker.provider; +import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.provider.Provider; +import javax.ws.rs.core.Response; + /** * @author Pedro Igor */ @@ -64,4 +67,6 @@ public interface IdentityProvider extends Provi * @return */ AuthenticationResponse handleResponse(AuthenticationRequest request); + + Response retrieveToken(FederatedIdentityModel identity); } diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java index 3118cfe2d8..14bc76753c 100644 --- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java @@ -25,7 +25,9 @@ import org.keycloak.broker.provider.AbstractIdentityProvider; import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationResponse; import org.keycloak.broker.provider.FederatedIdentity; +import org.keycloak.models.FederatedIdentityModel; +import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import java.io.IOException; @@ -100,7 +102,13 @@ public abstract class AbstractOAuth2IdentityProvider + @@ -21,6 +22,7 @@ + @@ -32,6 +34,14 @@ + + + + + + + + @@ -41,5 +51,8 @@ + + + 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 82c2ebc7ed..33698936e1 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java @@ -28,6 +28,7 @@ public class ApplicationRepresentation { protected Boolean fullScopeAllowed; protected Integer nodeReRegistrationTimeout; protected Map registeredNodes; + protected List allowedIdentityProviders; public String getId() { return id; @@ -188,4 +189,12 @@ public class ApplicationRepresentation { public void setFrontchannelLogout(Boolean frontchannelLogout) { this.frontchannelLogout = frontchannelLogout; } + + public List getAllowedIdentityProviders() { + return this.allowedIdentityProviders; + } + + public void setAllowedIdentityProviders(List allowedIdentityProviders) { + this.allowedIdentityProviders = allowedIdentityProviders; + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java index 023bd0114f..cc4b3e60b3 100644 --- a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java @@ -30,6 +30,7 @@ public class IdentityProviderRepresentation { protected String name; protected boolean enabled = true; protected boolean updateProfileFirstLogin = true; + protected boolean storeToken; protected String groupName; protected Map config = new HashMap(); @@ -65,14 +66,6 @@ public class IdentityProviderRepresentation { this.config = config; } - public String getGroupName() { - return this.groupName; - } - - public void setGroupName(String groupName) { - this.groupName = groupName; - } - public boolean isEnabled() { return this.enabled; } @@ -88,4 +81,20 @@ public class IdentityProviderRepresentation { public void setUpdateProfileFirstLogin(boolean updateProfileFirstLogin) { this.updateProfileFirstLogin = updateProfileFirstLogin; } + + public boolean isStoreToken() { + return this.storeToken; + } + + public void setStoreToken(boolean storeToken) { + this.storeToken = storeToken; + } + + public String getGroupName() { + return this.groupName; + } + + public void setGroupName(String groupName) { + this.groupName = groupName; + } } 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 de65ae6c2e..12ce2f4587 100755 --- a/core/src/main/java/org/keycloak/representations/idm/OAuthClientRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/OAuthClientRepresentation.java @@ -22,6 +22,7 @@ public class OAuthClientRepresentation { protected Boolean directGrantsOnly; protected Boolean fullScopeAllowed; protected Boolean frontchannelLogout; + protected List allowedIdentityProviders; public String getId() { @@ -135,4 +136,12 @@ public class OAuthClientRepresentation { public void setFrontchannelLogout(Boolean frontchannelLogout) { this.frontchannelLogout = frontchannelLogout; } + + public List getAllowedIdentityProviders() { + return this.allowedIdentityProviders; + } + + public void setAllowedIdentityProviders(List allowedIdentityProviders) { + this.allowedIdentityProviders = allowedIdentityProviders; + } } 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 d3cfbaf833..13db0219c2 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 @@ -464,6 +464,18 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'ApplicationCredentialsCtrl' }) + .when('/realms/:realm/applications/:application/identity-provider', { + templateUrl : 'partials/application-identity-provider.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + application : function(ApplicationLoader) { + return ApplicationLoader(); + } + }, + controller : 'ApplicationIdentityProviderCtrl' + }) .when('/realms/:realm/applications/:application/clustering', { templateUrl : 'partials/application-clustering.html', resolve : { @@ -750,6 +762,18 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'OAuthClientDetailCtrl' }) + .when('/realms/:realm/oauth-clients/:oauth/identity-provider', { + templateUrl : 'partials/oauth-client-identity-provider.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + oauth : function(OAuthClientLoader) { + return OAuthClientLoader(); + } + }, + controller : 'OAuthClientIdentityProviderCtrl' + }) .when('/realms/:realm/oauth-clients', { templateUrl : 'partials/oauth-client-list.html', resolve : { @@ -1050,6 +1074,48 @@ module.directive('onoffswitch', function() { } }); +/** + * Directive for presenting an ON-OFF switch for checkbox. + * This directive provides some additional capabilities to the default onoffswitch such as: + * + * - Dynamic values for id and name attributes. Useful if you need to use this directive inside a ng-repeat + * - Specific scope to specify the value. Instead of just true or false. + * + * Usage: + */ +module.directive('kc-onoffswitch-model', function() { + return { + restrict: "EA", + replace: true, + scope: { + name: '=', + id: '=', + value: '=', + ngModel: '=', + ngDisabled: '=', + kcOnText: '@onText', + kcOffText: '@offText' + }, + // TODO - The same code acts differently when put into the templateURL. Find why and move the code there. + //templateUrl: "templates/kc-switch.html", + template: "
", + compile: function(element, attrs) { + + if (!attrs.onText) { attrs.onText = "ON"; } + if (!attrs.offText) { attrs.offText = "OFF"; } + + element.bind('keydown click', function(e){ + var code = e.keyCode || e.which; + if (code === 32 || code === 13) { + e.stopImmediatePropagation(); + e.preventDefault(); + $(e.target).find('input').click(); + } + }); + } + } +}); + module.directive('kcInput', function() { var d = { scope : true, 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 784f1b821c..3f3a6cbc67 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 @@ -43,6 +43,77 @@ module.controller('ApplicationCredentialsCtrl', function($scope, $location, real }); }); +module.controller('ApplicationIdentityProviderCtrl', function($scope, $location, realm, application, Application, $location, Notifications) { + $scope.realm = realm; + $scope.application = angular.copy(application); + + $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; + + for (i = 0; i < $scope.application.allowedIdentityProviders.length; i++) { + var appProvider = $scope.application.allowedIdentityProviders[i]; + + if (appProvider == identityProvider.id) { + $scope.identityProviders[i] = identityProvider; + match = true; + } + } + + if (!match) { + var length = $scope.identityProviders.length; + + length = length + $scope.application.allowedIdentityProviders.length; + + $scope.identityProviders[length] = identityProvider; + } + } + + $scope.identityProviders = $scope.identityProviders.filter(function(n){ return n != undefined }); + + $scope.save = function() { + var selectedProviders = []; + + for (i = 0; i < $scope.application.allowedIdentityProviders.length; i++) { + var appProvider = $scope.application.allowedIdentityProviders[i]; + + if (appProvider) { + selectedProviders[selectedProviders.length] = appProvider; + } + } + + $scope.allowedIdentityProviders = $scope.application.allowedIdentityProviders; + $scope.application.allowedIdentityProviders = 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"); + Notifications.success("Your changes have been saved to the application."); + }); + }; + + $scope.reset = function() { + $scope.application = angular.copy(application); + $scope.changed = false; + }; + + $scope.$watch(function() { + return $location.path(); + }, function() { + $scope.path = $location.path().substring(1).split("/"); + }); +}); + module.controller('ApplicationSamlKeyCtrl', function($scope, $location, $http, $upload, realm, application, ApplicationCertificate, ApplicationCertificateGenerate, ApplicationCertificateDownload, Notifications) { 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 5fe70a76eb..dec42f56f3 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,4 +324,74 @@ module.controller('OAuthClientRevocationCtrl', function($scope, realm, oauth, OA } }); +module.controller('OAuthClientIdentityProviderCtrl', function($scope, realm, oauth, OAuthClient, $location, Notifications) { + $scope.realm = realm; + $scope.oauth = angular.copy(oauth); + + $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; + + for (i = 0; i < $scope.oauth.allowedIdentityProviders.length; i++) { + var appProvider = $scope.oauth.allowedIdentityProviders[i]; + + if (appProvider == identityProvider.id) { + $scope.identityProviders[i] = identityProvider; + match = true; + } + } + + if (!match) { + var length = $scope.identityProviders.length; + + length = length + $scope.oauth.allowedIdentityProviders.length; + + $scope.identityProviders[length] = identityProvider; + } + } + + $scope.identityProviders = $scope.identityProviders.filter(function(n){ return n != undefined }); + + $scope.save = function() { + var selectedProviders = []; + + for (i = 0; i < $scope.oauth.allowedIdentityProviders.length; i++) { + var appProvider = $scope.oauth.allowedIdentityProviders[i]; + + if (appProvider) { + selectedProviders[selectedProviders.length] = appProvider; + } + } + + $scope.allowedIdentityProviders = $scope.oauth.allowedIdentityProviders; + $scope.oauth.allowedIdentityProviders = 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"); + Notifications.success("Your changes have been saved to the application."); + }); + }; + + $scope.reset = function() { + $scope.oauth = angular.copy(oauth); + $scope.changed = false; + }; + + $scope.$watch(function() { + return $location.path(); + }, function() { + $scope.path = $location.path().substring(1).split("/"); + }); +}); 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 new file mode 100755 index 0000000000..6c90ef2a7a --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-identity-provider.html @@ -0,0 +1,24 @@ +
+
+ +
+ +

{{application.name}} Identity Provider Settings

+
+
+ {{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 new file mode 100755 index 0000000000..1a1462057b --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/oauth-client-identity-provider.html @@ -0,0 +1,24 @@ +
+
+ +
+ +

{{oauth.name}} Identity Provider Settings

+
+
+ {{identityProvider.name}} + +
+ +
+
+
+ +
+
+
+
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-oidc.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-oidc.html index 135c39282b..1e4de59ef9 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-oidc.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-oidc.html @@ -92,6 +92,13 @@ +
+ +
+ +
+ +
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-saml.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-saml.html index e5ba81f2ac..91cf006952 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-saml.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-saml.html @@ -93,6 +93,13 @@
+
+ +
+ +
+ +
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-social.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-social.html index 0cd6816345..e5a28e175d 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-social.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-social.html @@ -49,6 +49,13 @@
+
+ +
+ +
+ +
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/templates/kc-navigation-application.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/templates/kc-navigation-application.html index cb4d44a8b3..e84e1499bc 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/templates/kc-navigation-application.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/templates/kc-navigation-application.html @@ -6,6 +6,7 @@
  • Claims
  • Scope
  • Revocation
  • +
  • Identity Provider
  • Sessions
  • Clustering
  • Installation
  • diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/templates/kc-navigation-oauth-client.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/templates/kc-navigation-oauth-client.html index ffca27183d..db40ee5582 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/templates/kc-navigation-oauth-client.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/templates/kc-navigation-oauth-client.html @@ -4,5 +4,6 @@
  • Claims
  • Scope
  • Revocation
  • +
  • Identity Provider
  • Installation
  • \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl index e8cfbf3524..c881e9c9be 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl @@ -14,25 +14,35 @@ Personal Info:  <#list oauth.claimsRequested as claim> - ${claim}  + ${claim}  - <#list oauth.realmRolesRequested as role> + <#if oauth.accessRequestMessage??>
  • - <#if role.description??>${role.description}<#else>${role.name} + + ${oauth.accessRequestMessage} +
  • - - - <#list oauth.resourceRolesRequested?keys as resource> - <#list oauth.resourceRolesRequested[resource] as role> + + <#if oauth.realmRolesRequested??> + <#list oauth.realmRolesRequested as role>
  • - <#if role.description??>${role.description}<#else>${role.name} - in ${resource} + <#if role.description??>${role.description}<#else>${role.name}
  • - + + <#if oauth.resourceRolesRequested??> + <#list oauth.resourceRolesRequested?keys as resource> + <#list oauth.resourceRolesRequested[resource] as role> +
  • + <#if role.description??>${role.description}<#else>${role.name} + in ${resource} +
  • + + +
    diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java index 4fd2de967e..0d1f931268 100755 --- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java @@ -9,6 +9,7 @@ import org.keycloak.provider.Provider; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import java.net.URI; import java.util.List; /** @@ -39,6 +40,7 @@ public interface LoginFormsProvider extends Provider { public LoginFormsProvider setClientSessionCode(String accessCode); public LoginFormsProvider setAccessRequest(List realmRolesRequested, MultivaluedMap resourceRolesRequested); + public LoginFormsProvider setAccessRequest(String message); public LoginFormsProvider setError(String message); @@ -56,4 +58,5 @@ public interface LoginFormsProvider extends Provider { public LoginFormsProvider setStatus(Response.Status status); + LoginFormsProvider setActionUri(URI requestUri); } diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java index 704f8a757a..f897a5afe2 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -57,6 +57,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { private List realmRolesRequested; private MultivaluedMap resourceRolesRequested; private MultivaluedMap queryParams; + private String accessRequestMessage; + private URI actionUri; public static enum MessageType {SUCCESS, WARNING, ERROR} @@ -188,8 +190,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { if (realm != null) { attributes.put("realm", new RealmBean(realm)); - attributes.put("social", new IdentityProviderBean(realm, baseUri)); - attributes.put("url", new UrlBean(realm, theme, baseUri)); + attributes.put("social", new IdentityProviderBean(realm, baseUri, this.uriInfo)); + attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri)); } if (client != null) { @@ -209,7 +211,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { attributes.put("register", new RegisterBean(formData)); break; case OAUTH_GRANT: - attributes.put("oauth", new OAuthGrantBean(accessCode, client, realmRolesRequested, resourceRolesRequested)); + attributes.put("oauth", new OAuthGrantBean(accessCode, client, realmRolesRequested, resourceRolesRequested, this.accessRequestMessage)); break; case CODE: attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? message : null)); @@ -303,6 +305,12 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return this; } + @Override + public LoginFormsProvider setAccessRequest(String accessRequestMessage) { + this.accessRequestMessage = accessRequestMessage; + return this; + } + @Override public LoginFormsProvider setStatus(Response.Status status) { this.status = status; @@ -315,6 +323,12 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return this; } + @Override + public LoginFormsProvider setActionUri(URI actionUri) { + this.actionUri = actionUri; + return this; + } + @Override public void close() { } diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/IdentityProviderBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/IdentityProviderBean.java index 80f29bb82b..5a77a1a1df 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/IdentityProviderBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/IdentityProviderBean.java @@ -21,10 +21,13 @@ */ package org.keycloak.login.freemarker.model; +import org.keycloak.OAuth2Constants; +import org.keycloak.models.ClientModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.RealmModel; import org.keycloak.services.resources.flows.Urls; +import javax.ws.rs.core.UriInfo; import java.net.URI; import java.util.LinkedList; import java.util.List; @@ -39,7 +42,7 @@ public class IdentityProviderBean { private List providers; private RealmModel realm; - public IdentityProviderBean(RealmModel realm, URI baseURI) { + public IdentityProviderBean(RealmModel realm, URI baseURI, UriInfo uriInfo) { this.realm = realm; List identityProviders = realm.getIdentityProviders(); @@ -48,6 +51,16 @@ public class IdentityProviderBean { for (IdentityProviderModel identityProvider : identityProviders) { if (identityProvider.isEnabled()) { + String clientId = uriInfo.getQueryParameters().getFirst(OAuth2Constants.CLIENT_ID); + + if (clientId != null) { + ClientModel clientModel = realm.findClient(clientId); + + if (clientModel != null && !clientModel.hasIdentityProvider(identityProvider.getId())) { + continue; + } + } + String loginUrl = Urls.identityProviderAuthnRequest(baseURI, identityProvider, realm).toString(); providers.add(new IdentityProvider(identityProvider.getId(), identityProvider.getName(), loginUrl)); } diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java index f358d3df00..6d515a1d41 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java @@ -34,17 +34,19 @@ import java.util.List; */ public class OAuthGrantBean { + private final String accessRequestMessage; private List realmRolesRequested; private MultivaluedMap resourceRolesRequested; private String code; private ClientModel client; private List claimsRequested; - public OAuthGrantBean(String code, ClientModel client, List realmRolesRequested, MultivaluedMap resourceRolesRequested) { + public OAuthGrantBean(String code, ClientModel client, List realmRolesRequested, MultivaluedMap resourceRolesRequested, String accessRequestMessage) { this.code = code; this.client = client; this.realmRolesRequested = realmRolesRequested; this.resourceRolesRequested = resourceRolesRequested; + this.accessRequestMessage = accessRequestMessage; // todo support locale List claims = new LinkedList(); @@ -101,4 +103,8 @@ public class OAuthGrantBean { public List getClaimsRequested() { return claimsRequested; } + + public String getAccessRequestMessage() { + return this.accessRequestMessage; + } } diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java index bb8df0ec2e..26d67a3bc8 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java @@ -32,16 +32,18 @@ import java.net.URI; */ public class UrlBean { + private final URI actionuri; private URI baseURI; private Theme theme; private String realm; - public UrlBean(RealmModel realm, Theme theme, URI baseURI) { + public UrlBean(RealmModel realm, Theme theme, URI baseURI, URI actionUri) { this.realm = realm.getName(); this.theme = theme; this.baseURI = baseURI; + this.actionuri = actionUri; } public String getLoginAction() { @@ -85,6 +87,10 @@ public class UrlBean { } public String getOauthAction() { + if (this.actionuri != null) { + return this.actionuri.getPath(); + } + return Urls.realmOauthAction(baseURI, realm).toString(); } @@ -92,5 +98,4 @@ public class UrlBean { URI uri = Urls.themeRoot(baseURI); return uri.getPath() + "/" + theme.getType().toString().toLowerCase() +"/" + theme.getName(); } - } diff --git a/model/api/src/main/java/org/keycloak/models/ApplicationModel.java b/model/api/src/main/java/org/keycloak/models/ApplicationModel.java index 14cd081dd2..09f2a55e00 100755 --- a/model/api/src/main/java/org/keycloak/models/ApplicationModel.java +++ b/model/api/src/main/java/org/keycloak/models/ApplicationModel.java @@ -53,5 +53,4 @@ public interface ApplicationModel extends RoleContainerModel, ClientModel { void registerNode(String nodeHost, int registrationTime); void unregisterNode(String nodeHost); - } 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 ecbb48950f..0acd9c5a9c 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java @@ -1,5 +1,6 @@ package org.keycloak.models; +import java.util.List; import java.util.Map; import java.util.Set; @@ -97,4 +98,9 @@ public interface ClientModel { void setNotBefore(int notBefore); + void updateAllowedIdentityProviders(List identityProviders); + + List getAllowedIdentityProviders(); + + boolean hasIdentityProvider(String providerId); } diff --git a/model/api/src/main/java/org/keycloak/models/FederatedIdentityModel.java b/model/api/src/main/java/org/keycloak/models/FederatedIdentityModel.java index 77db720736..e720c40c7a 100755 --- a/model/api/src/main/java/org/keycloak/models/FederatedIdentityModel.java +++ b/model/api/src/main/java/org/keycloak/models/FederatedIdentityModel.java @@ -5,16 +5,20 @@ package org.keycloak.models; */ public class FederatedIdentityModel { - private String userId; - private String identityProvider; - private String userName; - - public FederatedIdentityModel() {}; + private final String token; + private final String userId; + private final String identityProvider; + private final String userName; public FederatedIdentityModel(String identityProvider, String userId, String userName) { + this(identityProvider, userId, userName, null); + } + + public FederatedIdentityModel(String providerId, String userId, String userName, String token) { + this.identityProvider = providerId; this.userId = userId; - this.identityProvider = identityProvider; this.userName = userName; + this.token = token; } public String getUserId() { @@ -28,4 +32,8 @@ public class FederatedIdentityModel { public String getUserName() { return userName; } + + public String getToken() { + return this.token; + } } diff --git a/model/api/src/main/java/org/keycloak/models/IdentityProviderModel.java b/model/api/src/main/java/org/keycloak/models/IdentityProviderModel.java index 11a95a27c1..76ca2d8a7d 100644 --- a/model/api/src/main/java/org/keycloak/models/IdentityProviderModel.java +++ b/model/api/src/main/java/org/keycloak/models/IdentityProviderModel.java @@ -50,6 +50,8 @@ public class IdentityProviderModel { private boolean updateProfileFirstLogin = true; + private boolean storeToken; + /** *

    A map containing the configuration and properties for a specific identity provider instance and implementation. The items * in the map are understood by the identity provider implementation.

    @@ -67,6 +69,7 @@ public class IdentityProviderModel { this.config = new HashMap(model.getConfig()); this.enabled = model.isEnabled(); this.updateProfileFirstLogin = model.isUpdateProfileFirstLogin(); + this.storeToken = model.isStoreToken(); } public String getInternalId() { @@ -117,6 +120,14 @@ public class IdentityProviderModel { this.updateProfileFirstLogin = updateProfileFirstLogin; } + public boolean isStoreToken() { + return this.storeToken; + } + + public void setStoreToken(boolean storeToken) { + this.storeToken = storeToken; + } + public Map getConfig() { return this.config; } 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 5b37c340fb..9cfceadc99 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 @@ -27,6 +27,7 @@ public class ClientEntity extends AbstractIdentifiableEntity { private List webOrigins = new ArrayList(); private List redirectUris = new ArrayList(); private List scopeIds = new ArrayList(); + private List allowedIdentityProviders; public String getName() { return name; @@ -139,4 +140,12 @@ public class ClientEntity extends AbstractIdentifiableEntity { public void setFrontchannelLogout(boolean frontchannelLogout) { this.frontchannelLogout = frontchannelLogout; } + + public List getAllowedIdentityProviders() { + return this.allowedIdentityProviders; + } + + public void setAllowedIdentityProviders(List allowedIdentityProviders) { + this.allowedIdentityProviders = allowedIdentityProviders; + } } diff --git a/model/api/src/main/java/org/keycloak/models/entities/FederatedIdentityEntity.java b/model/api/src/main/java/org/keycloak/models/entities/FederatedIdentityEntity.java index a8fa6473f3..02cdfac4d9 100644 --- a/model/api/src/main/java/org/keycloak/models/entities/FederatedIdentityEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/FederatedIdentityEntity.java @@ -8,6 +8,7 @@ public class FederatedIdentityEntity { private String userId; private String userName; private String identityProvider; + private String token; public String getUserId() { return userId; @@ -59,4 +60,12 @@ public class FederatedIdentityEntity { } return code; } + + public void setToken(String token) { + this.token = token; + } + + public String getToken() { + return token; + } } diff --git a/model/api/src/main/java/org/keycloak/models/entities/IdentityProviderEntity.java b/model/api/src/main/java/org/keycloak/models/entities/IdentityProviderEntity.java index 964a12c8eb..4fb33f28a9 100644 --- a/model/api/src/main/java/org/keycloak/models/entities/IdentityProviderEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/IdentityProviderEntity.java @@ -26,11 +26,13 @@ import java.util.Map; public class IdentityProviderEntity { private String internalId; + private String id; + private String providerId; private String name; private boolean enabled; private boolean updateProfileFirstLogin; - private String providerId; - private String id; + private boolean storeToken; + private Map config = new HashMap(); public String getInternalId() { @@ -65,6 +67,14 @@ public class IdentityProviderEntity { this.updateProfileFirstLogin = updateProfileFirstLogin; } + public boolean isStoreToken() { + return this.storeToken; + } + + public void setStoreToken(boolean storeToken) { + this.storeToken = storeToken; + } + public String getProviderId() { return providerId; } 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 42c4423275..8fe96880a0 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 @@ -258,6 +258,10 @@ public class ModelToRepresentation { rep.setRegisteredNodes(new HashMap(applicationModel.getRegisteredNodes())); } + if (!applicationModel.getAllowedIdentityProviders().isEmpty()) { + rep.setAllowedIdentityProviders(applicationModel.getAllowedIdentityProviders()); + } + return rep; } @@ -282,6 +286,11 @@ public class ModelToRepresentation { rep.setWebOrigins(new LinkedList(webOrigins)); } rep.setNotBefore(model.getNotBefore()); + + if (!model.getAllowedIdentityProviders().isEmpty()) { + rep.setAllowedIdentityProviders(model.getAllowedIdentityProviders()); + } + return rep; } 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 aede6a270d..0c7835ca56 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 @@ -112,6 +112,8 @@ public class RepresentationToModel { if (rep.getPasswordPolicy() != null) newRealm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy())); + importIdentityProviders(rep, newRealm); + if (rep.getApplications() != null) { Map appMap = createApplications(rep, newRealm); } @@ -231,21 +233,6 @@ public class RepresentationToModel { UserModel user = createUser(session, newRealm, userRep, appMap); } } - - if (rep.getIdentityProviders() != null) { - for (IdentityProviderRepresentation identityProviderRepresentation : rep.getIdentityProviders()) { - IdentityProviderModel identityProviderModel = new IdentityProviderModel(); - - identityProviderModel.setId(identityProviderRepresentation.getId()); - identityProviderModel.setProviderId(identityProviderRepresentation.getProviderId()); - identityProviderModel.setName(identityProviderRepresentation.getName()); - identityProviderModel.setEnabled(identityProviderRepresentation.isEnabled()); - identityProviderModel.setUpdateProfileFirstLogin(identityProviderRepresentation.isUpdateProfileFirstLogin()); - identityProviderModel.setConfig(identityProviderRepresentation.getConfig()); - - newRealm.addIdentityProvider(identityProviderModel); - } - } } public static void updateRealm(RealmRepresentation rep, RealmModel realm) { @@ -517,6 +504,10 @@ public class RepresentationToModel { if (rep.getClaims() != null) { setClaims(resource, rep.getClaims()); } + + if (rep.getAllowedIdentityProviders() != null) { + resource.updateAllowedIdentityProviders(rep.getAllowedIdentityProviders()); + } } public static void setClaims(ClientModel model, ClaimRepresentation rep) { @@ -632,6 +623,9 @@ public class RepresentationToModel { } } + if (rep.getAllowedIdentityProviders() != null) { + model.updateAllowedIdentityProviders(rep.getAllowedIdentityProviders()); + } } // Scope mappings @@ -747,4 +741,22 @@ public class RepresentationToModel { } } + + private static void importIdentityProviders(RealmRepresentation rep, RealmModel newRealm) { + if (rep.getIdentityProviders() != null) { + for (IdentityProviderRepresentation identityProviderRepresentation : rep.getIdentityProviders()) { + IdentityProviderModel identityProviderModel = new IdentityProviderModel(); + + identityProviderModel.setId(identityProviderRepresentation.getId()); + identityProviderModel.setProviderId(identityProviderRepresentation.getProviderId()); + identityProviderModel.setName(identityProviderRepresentation.getName()); + identityProviderModel.setEnabled(identityProviderRepresentation.isEnabled()); + identityProviderModel.setUpdateProfileFirstLogin(identityProviderRepresentation.isUpdateProfileFirstLogin()); + identityProviderModel.setStoreToken(identityProviderRepresentation.isStoreToken()); + identityProviderModel.setConfig(identityProviderRepresentation.getConfig()); + + newRealm.addIdentityProvider(identityProviderModel); + } + } + } } 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 dff08a685a..11696c5507 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 @@ -8,6 +8,7 @@ import org.keycloak.models.cache.entities.CachedClient; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -259,4 +260,22 @@ public abstract class ClientAdapter implements ClientModel { copy.putAll(cachedClient.getAttributes()); return copy; } + + @Override + public void updateAllowedIdentityProviders(List identityProviders) { + getDelegateForUpdate(); + updatedClient.updateAllowedIdentityProviders(identityProviders); + } + + @Override + public List getAllowedIdentityProviders() { + if (updatedClient != null) return updatedClient.getAllowedIdentityProviders(); + return cachedClient.getAllowedIdentityProviders(); + } + + @Override + public boolean hasIdentityProvider(String providerId) { + if (updatedClient != null) return updatedClient.hasIdentityProvider(providerId); + return cachedClient.hasIdentityProvider(providerId); + } } 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 b4b605eff0..ac7341d4a8 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 @@ -6,8 +6,10 @@ import org.keycloak.models.RealmProvider; import org.keycloak.models.RoleModel; import org.keycloak.models.cache.RealmCache; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -32,6 +34,7 @@ public class CachedClient { protected int notBefore; protected Set scope = new HashSet(); protected Set webOrigins = new HashSet(); + private List allowedIdentityProviders = new ArrayList(); public CachedClient(RealmCache cache, RealmProvider delegate, RealmModel realm, ClientModel model) { id = model.getId(); @@ -52,7 +55,7 @@ public class CachedClient { for (RoleModel role : model.getScopeMappings()) { scope.add(role.getId()); } - + this.allowedIdentityProviders = model.getAllowedIdentityProviders(); } public String getId() { @@ -122,4 +125,16 @@ public class CachedClient { public void setFrontchannelLogout(boolean frontchannelLogout) { this.frontchannelLogout = frontchannelLogout; } + + public List getAllowedIdentityProviders() { + return this.allowedIdentityProviders; + } + + public boolean hasIdentityProvider(String providerId) { + if (this.allowedIdentityProviders.isEmpty()) { + return true; + } + + return this.allowedIdentityProviders.contains(providerId); + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java index 9eab924577..dd1603c6af 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java @@ -7,9 +7,9 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; import org.keycloak.models.jpa.entities.ApplicationEntity; +import org.keycloak.models.jpa.entities.IdentityProviderEntity; import org.keycloak.models.jpa.entities.RoleEntity; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.util.Time; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; @@ -231,13 +231,6 @@ public class ApplicationAdapter extends ClientAdapter implements ApplicationMode em.flush(); } - public static boolean contains(String str, String[] array) { - for (String s : array) { - if (str.equals(s)) return true; - } - return false; - } - @Override public void updateDefaultRoles(String[] defaultRoles) { Collection entities = applicationEntity.getDefaultRoles(); 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 e71ba141d1..e8fb9436fa 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 @@ -5,11 +5,14 @@ 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.IdentityProviderEntity; import org.keycloak.models.jpa.entities.RoleEntity; import org.keycloak.models.jpa.entities.ScopeMappingEntity; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -293,4 +296,60 @@ public abstract class ClientAdapter implements ClientModel { copy.putAll(entity.getAttributes()); return copy; } + + @Override + public void updateAllowedIdentityProviders(List identityProviders) { + Collection entities = entity.getAllowedIdentityProviders(); + 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); + } else { + already.add(rel.getId()); + } + } + for (IdentityProviderEntity entity : remove) { + entities.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); + } + } + em.flush(); + } + + @Override + public List getAllowedIdentityProviders() { + Collection entities = entity.getAllowedIdentityProviders(); + List providers = new ArrayList(); + + for (IdentityProviderEntity entity : entities) { + providers.add(entity.getId()); + } + + return providers; + } + + @Override + public boolean hasIdentityProvider(String providerId) { + List allowedIdentityProviders = getAllowedIdentityProviders(); + + if (allowedIdentityProviders.isEmpty()) { + return true; + } + + return allowedIdentityProviders.contains(providerId); + } + + public static boolean contains(String str, String[] array) { + for (String s : array) { + if (str.equals(s)) return true; + } + return false; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index 60a79e62f7..098421ad88 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -96,6 +96,7 @@ public class JpaUserProvider implements UserProvider { entity.setIdentityProvider(identity.getIdentityProvider()); entity.setUserId(identity.getUserId()); entity.setUserName(identity.getUserName()); + entity.setToken(identity.getToken()); UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); entity.setUser(userEntity); em.persist(entity); @@ -344,7 +345,7 @@ public class JpaUserProvider implements UserProvider { List results = query.getResultList(); Set set = new HashSet(); for (FederatedIdentityEntity entity : results) { - set.add(new FederatedIdentityModel(entity.getIdentityProvider(), entity.getUserId(), entity.getUserName())); + set.add(new FederatedIdentityModel(entity.getIdentityProvider(), entity.getUserId(), entity.getUserName(), entity.getToken())); } return set; } @@ -352,7 +353,7 @@ public class JpaUserProvider implements UserProvider { @Override public FederatedIdentityModel getFederatedIdentity(UserModel user, String identityProvider, RealmModel realm) { FederatedIdentityEntity entity = findFederatedIdentity(user, identityProvider); - return (entity != null) ? new FederatedIdentityModel(entity.getIdentityProvider(), entity.getUserId(), entity.getUserName()) : null; + return (entity != null) ? new FederatedIdentityModel(entity.getIdentityProvider(), entity.getUserId(), entity.getUserName(), entity.getToken()) : null; } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 6cd1d0aa83..9f2b93bc84 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -1120,6 +1120,7 @@ public class RealmAdapter implements RealmModel { identityProviderModel.setConfig(entity.getConfig()); identityProviderModel.setEnabled(entity.isEnabled()); identityProviderModel.setUpdateProfileFirstLogin(entity.isUpdateProfileFirstLogin()); + identityProviderModel.setStoreToken(entity.isStoreToken()); identityProviders.add(identityProviderModel); } @@ -1174,6 +1175,7 @@ public class RealmAdapter implements RealmModel { entity.setName(identityProvider.getName()); entity.setEnabled(identityProvider.isEnabled()); entity.setUpdateProfileFirstLogin(identityProvider.isUpdateProfileFirstLogin()); + entity.setStoreToken(identityProvider.isStoreToken()); entity.setConfig(identityProvider.getConfig()); } } 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 8b32096780..ac436562ce 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 @@ -9,10 +9,14 @@ import javax.persistence.Id; import javax.persistence.Inheritance; import javax.persistence.InheritanceType; import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; import javax.persistence.ManyToOne; import javax.persistence.MapKeyColumn; +import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.UniqueConstraint; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -68,6 +72,10 @@ 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(); + public RealmEntity getRealm() { return realm; } @@ -179,4 +187,12 @@ public abstract class ClientEntity { public void setFrontchannelLogout(boolean frontchannelLogout) { this.frontchannelLogout = frontchannelLogout; } + + public Collection getAllowedIdentityProviders() { + return this.allowedIdentityProviders; + } + + public void setAllowedIdentityProviders(Collection allowedIdentityProviders) { + this.allowedIdentityProviders = allowedIdentityProviders; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/FederatedIdentityEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/FederatedIdentityEntity.java index 1a91888938..ccb14651b4 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/FederatedIdentityEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/FederatedIdentityEntity.java @@ -45,6 +45,9 @@ public class FederatedIdentityEntity { @Column(name = "FEDERATED_USERNAME") protected String userName; + @Column(name = "TOKEN") + protected String token; + public UserEntity getUser() { return user; } @@ -85,6 +88,14 @@ public class FederatedIdentityEntity { this.realmId = realmId; } + public void setToken(String token) { + this.token = token; + } + + public String getToken() { + return token; + } + public static class Key implements Serializable { protected UserEntity user; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java index 96d9c506e9..408d70655c 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java @@ -9,6 +9,8 @@ import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.MapKeyColumn; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; import javax.persistence.Table; import java.util.Map; @@ -17,6 +19,9 @@ import java.util.Map; */ @Entity @Table(name="IDENTITY_PROVIDER") +@NamedQueries({ + @NamedQuery(name="findIdentityProviderById", query="select identityProvider from IdentityProviderEntity identityProvider where identityProvider.id = :id") +}) public class IdentityProviderEntity { @Id @@ -42,6 +47,9 @@ public class IdentityProviderEntity { @Column(name="UPDATE_PROFILE_FIRST_LOGIN") private boolean updateProfileFirstLogin; + @Column(name="STORE_TOKEN") + private boolean storeToken; + @ElementCollection @MapKeyColumn(name="name") @Column(name="value", columnDefinition = "TEXT") @@ -104,6 +112,14 @@ public class IdentityProviderEntity { this.updateProfileFirstLogin = updateProfileFirstLogin; } + public boolean isStoreToken() { + return this.storeToken; + } + + public void setStoreToken(boolean storeToken) { + this.storeToken = storeToken; + } + public Map getConfig() { return this.config; } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java index 3f84c2b3d9..02e512a12a 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java @@ -11,7 +11,6 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.mongo.keycloak.entities.MongoApplicationEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; import org.keycloak.models.mongo.utils.MongoModelUtils; -import org.keycloak.util.Time; import java.util.ArrayList; import java.util.Collections; 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 ad56f84ff1..d3f93db902 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 @@ -291,5 +291,30 @@ public abstract class ClientAdapter extends A return copy; } + @Override + public void updateAllowedIdentityProviders(List identityProviders) { + List providerIds = new ArrayList(); + for (String providerId : identityProviders) { + providerIds.add(providerId); + } + getMongoEntityAsClient().setAllowedIdentityProviders(identityProviders); + updateMongoEntity(); + } + + @Override + public List getAllowedIdentityProviders() { + return getMongoEntityAsClient().getAllowedIdentityProviders(); + } + + @Override + public boolean hasIdentityProvider(String providerId) { + List allowedIdentityProviders = getMongoEntityAsClient().getAllowedIdentityProviders(); + + if (allowedIdentityProviders.isEmpty()) { + return true; + } + + return allowedIdentityProviders.contains(providerId); + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java index a542c2292d..b51fce3d77 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java @@ -6,10 +6,10 @@ import com.mongodb.QueryBuilder; import org.keycloak.connections.mongo.api.MongoStore; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.ApplicationModel; +import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; -import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; @@ -225,7 +225,7 @@ public class MongoUserProvider implements UserProvider { Set result = new HashSet(); for (FederatedIdentityEntity federatedIdentityEntity : linkEntities) { FederatedIdentityModel model = new FederatedIdentityModel(federatedIdentityEntity.getIdentityProvider(), - federatedIdentityEntity.getUserId(), federatedIdentityEntity.getUserName()); + federatedIdentityEntity.getUserId(), federatedIdentityEntity.getUserName(), federatedIdentityEntity.getToken()); result.add(model); } return result; @@ -296,13 +296,14 @@ public class MongoUserProvider implements UserProvider { @Override - public void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel socialLink) { + public void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel identity) { user = getUserById(user.getId(), realm); MongoUserEntity userEntity = ((UserAdapter) user).getUser(); FederatedIdentityEntity federatedIdentityEntity = new FederatedIdentityEntity(); - federatedIdentityEntity.setIdentityProvider(socialLink.getIdentityProvider()); - federatedIdentityEntity.setUserId(socialLink.getUserId()); - federatedIdentityEntity.setUserName(socialLink.getUserName()); + federatedIdentityEntity.setIdentityProvider(identity.getIdentityProvider()); + federatedIdentityEntity.setUserId(identity.getUserId()); + federatedIdentityEntity.setUserName(identity.getUserName()); + federatedIdentityEntity.setToken(identity.getToken()); getMongoStore().pushItemToList(userEntity, "federatedIdentities", federatedIdentityEntity, true, invocationContext); } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index de0a0cc8b5..44058be3b2 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -796,6 +796,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme identityProviderModel.setConfig(entity.getConfig()); identityProviderModel.setEnabled(entity.isEnabled()); identityProviderModel.setUpdateProfileFirstLogin(entity.isUpdateProfileFirstLogin()); + identityProviderModel.setStoreToken(entity.isStoreToken()); identityProviders.add(identityProviderModel); } @@ -850,6 +851,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme entity.setName(identityProvider.getName()); entity.setEnabled(identityProvider.isEnabled()); entity.setUpdateProfileFirstLogin(identityProvider.isUpdateProfileFirstLogin()); + entity.setStoreToken(identityProvider.isStoreToken()); entity.setConfig(identityProvider.getConfig()); } } diff --git a/services/src/main/java/org/keycloak/services/resources/AuthenticationBrokerResource.java b/services/src/main/java/org/keycloak/services/resources/AuthenticationBrokerResource.java index 0f14b30d01..17c05e5fae 100644 --- a/services/src/main/java/org/keycloak/services/resources/AuthenticationBrokerResource.java +++ b/services/src/main/java/org/keycloak/services/resources/AuthenticationBrokerResource.java @@ -28,28 +28,35 @@ import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; +import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.OAuthClientModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.provider.ProviderFactory; +import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.AuthenticationManager.AuthResult; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.EventsManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.flows.Flows; import org.keycloak.social.SocialIdentityProvider; +import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; @@ -100,6 +107,13 @@ public class AuthenticationBrokerResource { try { ClientSessionModel clientSession = clientCode.getClientSession(); + ClientModel clientModel = clientSession.getClient(); + Response response = checkClientPermissions(clientModel, providerId); + + if (response != null) { + return response; + } + IdentityProvider identityProvider = getIdentityProvider(realm, providerId); if (identityProvider == null) { @@ -109,7 +123,8 @@ public class AuthenticationBrokerResource { AuthenticationResponse authenticationResponse = identityProvider.handleRequest(createAuthenticationRequest(providerId, code, realm, clientSession)); - Response response = authenticationResponse.getResponse(); + + response = authenticationResponse.getResponse(); if (response != null) { event.success(); @@ -142,6 +157,67 @@ public class AuthenticationBrokerResource { return handleResponse(realmName, providerId); } + @GET + @Path("{realm}/{provider_id}/token") + public Response retrieveToken(@PathParam("realm") final String realmName, @PathParam("provider_id") String providerId) { + return getToken(realmName, providerId, false); + } + + private Response getToken(String realmName, String providerId, boolean forceRetrieval) { + RealmManager realmManager = new RealmManager(session); + RealmModel realm = realmManager.getRealmByName(realmName); + AppAuthManager authManager = new AppAuthManager(); + AuthResult authResult = authManager.authenticateBearerToken(session, realm, uriInfo, clientConnection, request.getHttpHeaders()); + + if (authResult != null) { + String audience = authResult.getToken().getAudience(); + ClientModel clientModel = realm.findClient(audience); + + if (clientModel == null) { + return Flows.errors().error("Invalid client.", Response.Status.FORBIDDEN); + } + + if (!clientModel.hasIdentityProvider(providerId)) { + return Flows.errors().error("Client [" + audience + "] not authorized.", Response.Status.FORBIDDEN); + } + + if (OAuthClientModel.class.isInstance(clientModel) && !forceRetrieval) { + return Flows.forms(session, realm, clientModel, uriInfo) + .setClientSessionCode(authManager.extractAuthorizationHeaderToken(request.getHttpHeaders())) + .setAccessRequest("Your information from " + providerId + " identity provider.") + .setClient(clientModel) + .setUriInfo(this.uriInfo) + .setActionUri(this.uriInfo.getRequestUri()) + .createOAuthGrant(); + } + + IdentityProvider identityProvider = getIdentityProvider(realm, providerId); + IdentityProviderModel identityProviderConfig = getIdentityProviderConfig(realm, providerId); + + if (identityProviderConfig.isStoreToken()) { + FederatedIdentityModel identity = this.session.users().getFederatedIdentity(authResult.getUser(), providerId, realm); + + return identityProvider.retrieveToken(identity); + } + + return Flows.errors().error("Identity Provider [" + providerId + "] does not support this operation.", Response.Status.FORBIDDEN); + } + + return Flows.errors().error("Invalid code.", Response.Status.FORBIDDEN); + } + + @POST + @Path("{realm}/{provider_id}/token") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response consentTokenRetrieval(@PathParam("realm") final String realmName, @PathParam("provider_id") String providerId, + final MultivaluedMap formData) { + if (formData.containsKey("cancel")) { + return Flows.errors().error("Permission not approved.", Response.Status.FORBIDDEN); + } + + return getToken(realmName, providerId, true); + } + private Response handleResponse(String realmName, String providerId) { RealmManager realmManager = new RealmManager(session); RealmModel realm = realmManager.getRealmByName(realmName); @@ -166,17 +242,24 @@ public class AuthenticationBrokerResource { } ClientSessionModel clientSession = clientCode.getClientSession(); - - AuthenticationResponse authenticationResponse = provider.handleResponse(createAuthenticationRequest(providerId, null, realm, clientSession)); - Response response = authenticationResponse.getResponse(); + ClientModel clientModel = clientSession.getClient(); + Response response = checkClientPermissions(clientModel, providerId); if (response != null) { return response; } - FederatedIdentity socialUser = authenticationResponse.getUser(); + AuthenticationResponse authenticationResponse = provider.handleResponse(createAuthenticationRequest(providerId, null, realm, clientSession)); - return performLocalAuthentication(realm, providerId, socialUser, clientCode); + response = authenticationResponse.getResponse(); + + if (response != null) { + return response; + } + + FederatedIdentity identity = authenticationResponse.getUser(); + + return performLocalAuthentication(realm, providerId, identity, clientCode); } catch (Exception e) { if (session.getTransaction().isActive()) { session.getTransaction().rollback(); @@ -192,14 +275,14 @@ public class AuthenticationBrokerResource { } } - private Response performLocalAuthentication(RealmModel realm, String providerId, FederatedIdentity socialUser, ClientSessionCode clientCode) { + private Response performLocalAuthentication(RealmModel realm, String providerId, FederatedIdentity identity, ClientSessionCode clientCode) { ClientSessionModel clientSession = clientCode.getClientSession(); - FederatedIdentityModel socialLink = new FederatedIdentityModel(providerId, socialUser.getId(), - socialUser.getUsername()); - UserModel federatedUser = session.users().getUserByFederatedIdentity(socialLink, realm); + FederatedIdentityModel identityModel = new FederatedIdentityModel(providerId, identity.getId(), + identity.getUsername(), identity.getToken()); + UserModel federatedUser = session.users().getUserByFederatedIdentity(identityModel, realm); IdentityProviderModel identityProviderConfig = getIdentityProviderConfig(realm, providerId); - String authMethod = socialLink.getUserId() + "@" + identityProviderConfig.getId(); + String authMethod = identityModel.getUserId() + "@" + identityProviderConfig.getId(); EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder() .event(EventType.LOGIN) .client(clientSession.getClient()) @@ -228,29 +311,29 @@ public class AuthenticationBrokerResource { return redirectToErrorPage(realm, "Insufficient permissions to link identity"); } - session.users().addFederatedIdentity(realm, authenticatedUser, socialLink); + session.users().addFederatedIdentity(realm, authenticatedUser, identityModel); event.success(); return Response.status(302).location(UriBuilder.fromUri(clientSession.getRedirectUri()).build()).build(); } - UserModel user = session.users().getUserByEmail(socialUser.getEmail(), realm); + UserModel user = session.users().getUserByEmail(identity.getEmail(), realm); String errorMessage = "federatedIdentityEmailExists"; if (user == null) { - user = session.users().getUserByUsername(socialUser.getUsername(), realm); + user = session.users().getUserByUsername(identity.getUsername(), realm); errorMessage = "federatedIdentityUsernameExists"; } if (user == null) { - federatedUser = session.users().addUser(realm, socialUser.getUsername()); + federatedUser = session.users().addUser(realm, identity.getUsername()); federatedUser.setEnabled(true); - federatedUser.setFirstName(socialUser.getFirstName()); - federatedUser.setLastName(socialUser.getLastName()); - federatedUser.setEmail(socialUser.getEmail()); + federatedUser.setFirstName(identity.getFirstName()); + federatedUser.setLastName(identity.getLastName()); + federatedUser.setEmail(identity.getEmail()); - session.users().addFederatedIdentity(realm, federatedUser, socialLink); + session.users().addFederatedIdentity(realm, federatedUser, identityModel); event.clone().user(federatedUser).event(EventType.REGISTER) .detail(Details.REGISTER_METHOD, authMethod) @@ -272,7 +355,7 @@ public class AuthenticationBrokerResource { event.user(federatedUser); - String username = socialLink.getUserId() + "@" + identityProviderConfig.getName(); + String username = identityModel.getUserId() + "@" + identityProviderConfig.getName(); UserSessionModel userSession = session.sessions() .createUserSession(realm, federatedUser, username, clientConnection.getRemoteAddr(), "broker", false); @@ -353,4 +436,16 @@ public class AuthenticationBrokerResource { return null; } + private Response checkClientPermissions(ClientModel clientModel, String providerId) { + if (clientModel == null) { + return Flows.errors().error("Invalid client.", Response.Status.FORBIDDEN); + } + + if (!clientModel.hasIdentityProvider(providerId)) { + return Flows.errors().error("Client [" + clientModel.getClientId() + "] not authorized.", Response.Status.FORBIDDEN); + } + + return null; + } + } 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 fc0e6cac0b..1917844f95 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 @@ -5,6 +5,7 @@ import org.jboss.resteasy.plugins.providers.multipart.InputPart; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; +import org.keycloak.models.ClientModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -83,6 +84,7 @@ public class IdentityProviderResource { String providerId = formDataMap.get("providerId").get(0).getBodyAsString(); String enabled = formDataMap.get("enabled").get(0).getBodyAsString(); String updateProfileFirstLogin = formDataMap.get("updateProfileFirstLogin").get(0).getBodyAsString(); + String storeToken = formDataMap.get("storeToken").get(0).getBodyAsString(); InputPart file = formDataMap.get("file").get(0); InputStream inputStream = file.getBody(InputStream.class, null); IdentityProviderFactory providerFactory = getProviderFactorytById(providerId); @@ -94,6 +96,7 @@ public class IdentityProviderResource { providerModel.setProviderId(providerId); providerModel.setEnabled(Boolean.valueOf(enabled)); providerModel.setUpdateProfileFirstLogin(Boolean.valueOf(updateProfileFirstLogin)); + providerModel.setStoreToken(Boolean.valueOf(storeToken)); providerModel.setConfig(config); return create(uriInfo, providerModel); @@ -117,10 +120,32 @@ public class IdentityProviderResource { @DELETE @NoCache public Response delete(@PathParam("id") String providerId) { + for (ClientModel applicationModel : getClientModels()) { + List allowedIdentityProviders = applicationModel.getAllowedIdentityProviders(); + + for (String appProvider : new ArrayList(allowedIdentityProviders)) { + if (appProvider.equals(providerId)) { + allowedIdentityProviders.remove(appProvider); + } + } + + applicationModel.updateAllowedIdentityProviders(allowedIdentityProviders); + } + this.realm.removeIdentityProviderById(providerId); + return Response.noContent().build(); } + private List getClientModels() { + List clients = new ArrayList(); + + clients.addAll(this.realm.getOAuthClients()); + clients.addAll(this.realm.getApplications()); + + return clients; + } + @PUT @Consumes("application/json") public Response update(IdentityProviderModel model) { diff --git a/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java index 8c070be25d..5266c68a4e 100755 --- a/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java +++ b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java @@ -24,8 +24,7 @@ public class FacebookIdentityProvider extends AbstractOAuth2IdentityProvider imp config.setUserInfoUrl(PROFILE_URL); } - @Override - protected FederatedIdentity getFederatedIdentity(String accessToken) { + protected FederatedIdentity doGetFederatedIdentity(String accessToken) { try { JsonNode profile = SimpleHttp.doGet(PROFILE_URL).header("Authorization", "Bearer " + accessToken).asJson(); diff --git a/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java b/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java index 5069591da1..7be16dcfbf 100755 --- a/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java +++ b/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java @@ -25,7 +25,7 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple } @Override - protected FederatedIdentity getFederatedIdentity(String accessToken) { + protected FederatedIdentity doGetFederatedIdentity(String accessToken) { try { JsonNode profile = SimpleHttp.doGet(PROFILE_URL).header("Authorization", "Bearer " + accessToken).asJson(); diff --git a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java index 0e7673cf0b..1b7a563194 100755 --- a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java +++ b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java @@ -27,12 +27,14 @@ import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationResponse; import org.keycloak.broker.provider.FederatedIdentity; import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.FederatedIdentityModel; import org.keycloak.social.SocialIdentityProvider; import twitter4j.Twitter; import twitter4j.TwitterFactory; import twitter4j.auth.RequestToken; import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.net.URI; @@ -109,4 +111,9 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realm); + + assertFalse(federatedIdentities.isEmpty()); + assertEquals(1, federatedIdentities.size()); + + FederatedIdentityModel identityModel = federatedIdentities.iterator().next(); + + assertNotNull(identityModel.getToken()); + + UserSessionStatus userSessionStatus = retrieveSessionStatus(); + String accessToken = userSessionStatus.getAccessTokenString(); + String tokenEndpointUrl = "http://localhost:8081/auth/broker/realm-with-broker/" + getProviderId() + "/token"; + final String authHeader = "Bearer " + accessToken; + ClientRequestFilter authFilter = new ClientRequestFilter() { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader); + } + }; + Client client = ClientBuilder.newBuilder().register(authFilter).build(); + UriBuilder authBase = UriBuilder.fromUri(tokenEndpointUrl); + WebTarget tokenEndpoint = client.target(authBase); + Response response = tokenEndpoint.request().get(); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertNotNull(response.readEntity(String.class)); + + driver.navigate().to("http://localhost:8081/test-app/logout"); + driver.navigate().to("http://localhost:8081/test-app"); + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/login")); + } + + @Test + public void testTokenStorageAndRetrievalByOAuthClient() { + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + + identityProviderModel.setStoreToken(true); + identityProviderModel.setUpdateProfileFirstLogin(false); + + driver.navigate().to("http://localhost:8081/test-app"); + + // choose the identity provider + this.loginPage.clickSocial(getProviderId()); + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/")); + + // log in to identity provider + this.loginPage.login("test-user", "password"); + + doAfterProviderAuthentication(); + + changePasswordPage.realm("realm-with-broker"); + changePasswordPage.open(); + changePasswordPage.changePassword("password", "password"); + + driver.navigate().to("http://localhost:8081/test-app/logout"); + + oauth.realm("realm-with-broker"); + oauth.redirectUri("http://localhost:8081/third-party"); + oauth.clientId("third-party"); + oauth.doLoginGrant("test-user@localhost", "password"); + + grantPage.assertCurrent(); + grantPage.accept(); + + assertTrue(oauth.getCurrentQuery().containsKey(OAuth2Constants.CODE)); + AccessTokenResponse accessToken = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password"); + String tokenEndpointUrl = "http://localhost:8081/auth/broker/realm-with-broker/" + getProviderId() + "/token"; + String authHeader = "Bearer " + accessToken.getAccessToken(); + HtmlUnitDriver htmlUnitDriver = (WebRule.HtmlUnitDriver) this.driver; + + htmlUnitDriver.getWebClient().addRequestHeader(HttpHeaders.AUTHORIZATION, authHeader); + + htmlUnitDriver.navigate().to(tokenEndpointUrl); + + grantPage.assertCurrent(); + grantPage.accept(); + + assertNotNull(driver.getPageSource()); + + doAssertTokenRetrieval(driver.getPageSource()); + } + + protected abstract void doAssertTokenRetrieval(String pageSource); + private void assertSuccessfulAuthentication(IdentityProviderModel identityProviderModel) { + authenticateWithIdentityProvider(identityProviderModel); + + // authenticated and redirected to app + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); + + UserModel federatedUser = getFederatedUser(); + + assertNotNull(federatedUser); + + doAssertFederatedUser(federatedUser, identityProviderModel); + + RealmModel realm = getRealm(); + + Set federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realm); + + assertEquals(1, federatedIdentities.size()); + + FederatedIdentityModel federatedIdentityModel = federatedIdentities.iterator().next(); + + assertEquals(getProviderId(), federatedIdentityModel.getIdentityProvider()); + assertEquals(federatedUser.getUsername(), federatedIdentityModel.getUserName()); + + driver.navigate().to("http://localhost:8081/test-app/logout"); + driver.navigate().to("http://localhost:8081/test-app"); + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/login")); + } + + private void authenticateWithIdentityProvider(IdentityProviderModel identityProviderModel) { driver.navigate().to("http://localhost:8081/test-app"); assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/login")); @@ -209,31 +360,6 @@ public abstract class AbstractIdentityProviderTest { this.updateProfilePage.assertCurrent(); this.updateProfilePage.update(userFirstName, userLastName, userEmail); } - - // authenticated and redirected to app - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); - - UserModel federatedUser = getFederatedUser(); - - assertNotNull(federatedUser); - - doAssertFederatedUser(federatedUser); - - RealmModel realm = getRealm(); - - Set federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realm); - - assertEquals(1, federatedIdentities.size()); - - FederatedIdentityModel federatedIdentityModel = federatedIdentities.iterator().next(); - - assertEquals(getProviderId(), federatedIdentityModel.getIdentityProvider()); - assertEquals(federatedUser.getUsername(), federatedIdentityModel.getUserName()); - - driver.navigate().to("http://localhost:8081/test-app/logout"); - driver.navigate().to("http://localhost:8081/test-app"); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/login")); } protected UserModel getFederatedUser() { @@ -256,6 +382,9 @@ public abstract class AbstractIdentityProviderTest { assertNotNull(identityProviderModel); + identityProviderModel.setUpdateProfileFirstLogin(true); + identityProviderModel.setEnabled(true); + return identityProviderModel; } @@ -263,9 +392,7 @@ public abstract class AbstractIdentityProviderTest { return this.session.realms().getRealm("realm-with-broker"); } - protected void doAssertFederatedUser(UserModel federatedUser) { - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - + protected void doAssertFederatedUser(UserModel federatedUser, IdentityProviderModel identityProviderModel) { if (identityProviderModel.isUpdateProfileFirstLogin()) { String userEmail = "new@email.com"; String userFirstName = "New first"; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/BrokerKeyCloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/BrokerKeyCloakRule.java index be9503d08b..deb261143f 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/BrokerKeyCloakRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/BrokerKeyCloakRule.java @@ -35,6 +35,7 @@ public class BrokerKeyCloakRule extends AbstractKeycloakRule { server.importRealm(getClass().getResourceAsStream("/broker-test/test-realm-with-broker.json")); URL url = getClass().getResource("/broker-test/test-app-keycloak.json"); deployApplication("test-app", "/test-app", UserSessionStatusServlet.class, url.getPath(), "manager"); + deployApplication("test-app-allowed-providers", "/test-app-allowed-providers", UserSessionStatusServlet.class, url.getPath(), "manager"); } @Override 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 d8e759460b..9a646cb32d 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 @@ -76,6 +76,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes identityProviderModel.getConfig().put("config-added", "value-added"); identityProviderModel.setEnabled(false); identityProviderModel.setUpdateProfileFirstLogin(false); + identityProviderModel.setStoreToken(true); realm.updateIdentityProvider(identityProviderModel); @@ -87,8 +88,9 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes assertEquals("Changed Name", identityProviderModel.getName()); assertEquals("value-added", identityProviderModel.getConfig().get("config-added")); - assertEquals(false, identityProviderModel.isEnabled()); - assertEquals(false, identityProviderModel.isUpdateProfileFirstLogin()); + assertFalse(identityProviderModel.isEnabled()); + assertFalse(identityProviderModel.isUpdateProfileFirstLogin()); + assertTrue(identityProviderModel.isStoreToken()); identityProviderModel.setName("Changed Name Again"); identityProviderModel.getConfig().remove("config-added"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java index 011af1b30f..66e4b7183d 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java @@ -3,11 +3,18 @@ package org.keycloak.testsuite.broker; import org.junit.ClassRule; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.representations.AccessTokenResponse; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.pages.OAuthGrantPage; import org.keycloak.testsuite.rule.AbstractKeycloakRule; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testutils.KeycloakServer; +import org.keycloak.util.JsonSerialization; + +import java.io.IOException; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; /** * @author pedroigor @@ -38,6 +45,18 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractIdentityProviderT grantPage.accept(); } + @Override + protected void doAssertTokenRetrieval(String pageSource) { + try { + AccessTokenResponse accessTokenResponse = JsonSerialization.readValue(pageSource, AccessTokenResponse.class); + + assertNotNull(accessTokenResponse.getToken()); + assertNotNull(accessTokenResponse.getIdToken()); + } catch (IOException e) { + fail("Could not parse token."); + } + } + @Override protected String getProviderId() { return "kc-oidc-idp"; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java index b14328a4c2..77836655bd 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java @@ -8,9 +8,17 @@ import org.keycloak.models.UserModel; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.rule.AbstractKeycloakRule; import org.keycloak.testutils.KeycloakServer; +import org.picketlink.identity.federation.api.saml.v2.request.SAML2Request; +import org.picketlink.identity.federation.saml.v2.protocol.ResponseType; +import org.picketlink.identity.federation.web.util.PostBindingUtil; + +import java.net.URLDecoder; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; /** * @author pedroigor @@ -37,15 +45,27 @@ public class SAMLKeyCloakServerBrokerBasicTest extends AbstractIdentityProviderT } @Override - protected void doAssertFederatedUser(UserModel federatedUser) { - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - + protected void doAssertFederatedUser(UserModel federatedUser, IdentityProviderModel identityProviderModel) { if (identityProviderModel.isUpdateProfileFirstLogin()) { - super.doAssertFederatedUser(federatedUser); + super.doAssertFederatedUser(federatedUser, identityProviderModel); } else { assertEquals("test-user@localhost", federatedUser.getEmail()); assertNull(federatedUser.getFirstName()); assertNull(federatedUser.getLastName()); } } + + @Override + protected void doAssertTokenRetrieval(String pageSource) { + try { + SAML2Request saml2Request = new SAML2Request(); + ResponseType responseType = (ResponseType) saml2Request + .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8"))); + + assertNotNull(responseType); + assertFalse(responseType.getAssertions().isEmpty()); + } catch (Exception e) { + fail("Could not parse token."); + } + } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java index 47ddb14d31..e387097316 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java @@ -8,9 +8,17 @@ import org.keycloak.models.UserModel; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.rule.AbstractKeycloakRule; import org.keycloak.testutils.KeycloakServer; +import org.picketlink.identity.federation.api.saml.v2.request.SAML2Request; +import org.picketlink.identity.federation.saml.v2.protocol.ResponseType; +import org.picketlink.identity.federation.web.util.PostBindingUtil; + +import java.net.URLDecoder; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; /** * @author pedroigor @@ -37,15 +45,27 @@ public class SAMLKeyCloakServerBrokerWithSignatureTest extends AbstractIdentityP } @Override - protected void doAssertFederatedUser(UserModel federatedUser) { - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - + protected void doAssertFederatedUser(UserModel federatedUser, IdentityProviderModel identityProviderModel) { if (identityProviderModel.isUpdateProfileFirstLogin()) { - super.doAssertFederatedUser(federatedUser); + super.doAssertFederatedUser(federatedUser, identityProviderModel); } else { assertEquals("test-user@localhost", federatedUser.getEmail()); assertNull(federatedUser.getFirstName()); assertNull(federatedUser.getLastName()); } } + + @Override + protected void doAssertTokenRetrieval(String pageSource) { + try { + SAML2Request saml2Request = new SAML2Request(); + ResponseType responseType = (ResponseType) saml2Request + .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8"))); + + assertNotNull(responseType); + assertFalse(responseType.getAssertions().isEmpty()); + } catch (Exception e) { + fail("Could not parse token."); + } + } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/CustomIdentityProvider.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/CustomIdentityProvider.java index 4ef9241433..0c0e3692d4 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/CustomIdentityProvider.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/CustomIdentityProvider.java @@ -20,8 +20,11 @@ package org.keycloak.testsuite.broker.provider; import org.keycloak.broker.provider.AbstractIdentityProvider; import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationResponse; +import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; +import javax.ws.rs.core.Response; + /** * @author pedroigor */ @@ -45,4 +48,9 @@ public class CustomIdentityProvider extends AbstractIdentityProvider