From 081db0d35371e9f4ebfdd8ef4241c41d7da7571d Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 8 Dec 2015 15:27:02 +0100 Subject: [PATCH 1/2] KEYCLOAK-2124 Post-Broker login flow support --- .../META-INF/jpa-changelog-1.8.0.xml | 12 + .../META-INF/jpa-changelog-master.xml | 1 + .../idm/IdentityProviderRepresentation.java | 9 + .../java/org/keycloak/events/EventType.java | 2 + .../messages/admin-messages_en.properties | 2 + .../admin/resources/js/controllers/realm.js | 7 + .../realm-identity-provider-oidc.html | 12 + .../realm-identity-provider-saml.html | 12 + .../realm-identity-provider-social.html | 12 + .../models/IdentityProviderModel.java | 11 + .../entities/IdentityProviderEntity.java | 9 + .../models/utils/ModelToRepresentation.java | 9 + .../models/utils/RepresentationToModel.java | 11 + .../org/keycloak/models/jpa/RealmAdapter.java | 3 + .../jpa/entities/IdentityProviderEntity.java | 11 + .../mongo/keycloak/adapters/RealmAdapter.java | 3 + .../broker/AbstractIdpAuthenticator.java | 4 +- .../broker/IdpReviewProfileAuthenticator.java | 2 +- .../broker/IdpUsernamePasswordForm.java | 2 +- .../broker/util/PostBrokerLoginConstants.java | 18 ++ .../SerializedBrokeredIdentityContext.java | 8 +- .../main/java/org/keycloak/services/Urls.java | 7 + .../resources/IdentityBrokerService.java | 150 +++++++-- .../resources/LoginActionsService.java | 73 ++++- .../broker/AbstractFirstBrokerLoginTest.java | 2 +- .../broker/AbstractIdentityProviderTest.java | 4 +- .../broker/ImportIdentityProviderTest.java | 5 + .../testsuite/broker/PostBrokerFlowTest.java | 302 ++++++++++++++++++ .../testsuite/pages/LoginTotpPage.java | 2 +- .../broker-test/test-realm-with-broker.json | 1 + 30 files changed, 646 insertions(+), 60 deletions(-) create mode 100644 connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/broker/util/PostBrokerLoginConstants.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/broker/PostBrokerFlowTest.java diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml new file mode 100644 index 0000000000..c4dce2b8f5 --- /dev/null +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml index 2acc0bba74..0f907e0e02 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml @@ -11,4 +11,5 @@ + 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 0dc7f0a019..c227abcb61 100755 --- a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java @@ -54,6 +54,7 @@ public class IdentityProviderRepresentation { protected boolean addReadTokenRoleOnCreate; protected boolean authenticateByDefault; protected String firstBrokerLoginFlowAlias; + protected String postBrokerLoginFlowAlias; protected Map config = new HashMap(); public String getInternalId() { @@ -139,6 +140,14 @@ public class IdentityProviderRepresentation { this.firstBrokerLoginFlowAlias = firstBrokerLoginFlowAlias; } + public String getPostBrokerLoginFlowAlias() { + return postBrokerLoginFlowAlias; + } + + public void setPostBrokerLoginFlowAlias(String postBrokerLoginFlowAlias) { + this.postBrokerLoginFlowAlias = postBrokerLoginFlowAlias; + } + public boolean isStoreToken() { return this.storeToken; } diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java index b75728bd15..599dec5bf3 100755 --- a/events/api/src/main/java/org/keycloak/events/EventType.java +++ b/events/api/src/main/java/org/keycloak/events/EventType.java @@ -64,6 +64,8 @@ public enum EventType { IDENTITY_PROVIDER_LOGIN_ERROR(false), IDENTITY_PROVIDER_FIRST_LOGIN(true), IDENTITY_PROVIDER_FIRST_LOGIN_ERROR(true), + IDENTITY_PROVIDER_POST_LOGIN(true), + IDENTITY_PROVIDER_POST_LOGIN_ERROR(true), IDENTITY_PROVIDER_RESPONSE(false), IDENTITY_PROVIDER_RESPONSE_ERROR(false), IDENTITY_PROVIDER_RETRIEVE_TOKEN(false), diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 0dd5168b7d..287abe48e2 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -385,6 +385,7 @@ add-provider.placeholder=Add provider... provider=Provider gui-order=GUI order first-broker-login-flow=First Login Flow +post-broker-login-flow=Post Login Flow redirect-uri=Redirect URI redirect-uri.tooltip=The redirect uri to use when configuring the identity provider. alias=Alias @@ -405,6 +406,7 @@ trust-email=Trust Email trust-email.tooltip=If enabled then email provided by this provider is not verified even if verification is enabled for the realm. gui-order.tooltip=Number defining order of the provider in GUI (eg. on Login page). first-broker-login-flow.tooltip=Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that there is not yet existing Keycloak account linked with the authenticated identity provider account. +post-broker-login-flow.tooltip=Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user authenticated with this identity provider (for example OTP). Leave this empty if you don't want any additional authenticators to be triggered after login with this identity provider. Also note, that authenticator implementations must assume that user is already set in ClientSession as identity provider already set it. openid-connect-config=OpenID Connect Config openid-connect-config.tooltip=OIDC SP and external IDP configuration. authorization-url=Authorization URL diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index a66e205322..445b8d0cef 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -695,6 +695,13 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload } } + $scope.postBrokerAuthFlows = []; + var emptyFlow = { alias: "" }; + $scope.postBrokerAuthFlows.push(emptyFlow); + for (var i=0 ; i<$scope.authFlows.length ; i++) { + $scope.postBrokerAuthFlows.push($scope.authFlows[i]); + } + $scope.$watch(function() { return $location.path(); }, function() { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html index 4cbd12a09f..1bcf81bba8 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html @@ -79,6 +79,18 @@ {{:: 'first-broker-login-flow.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'post-broker-login-flow.tooltip' | translate}} +
{{:: 'openid-connect-config' | translate}} {{:: 'openid-connect-config.tooltip' | translate}} diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html index c8f6c3771a..2abfeb8b85 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html @@ -79,6 +79,18 @@ {{:: 'first-broker-login-flow.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'post-broker-login-flow.tooltip' | translate}} +
{{:: 'saml-config' | translate}} {{:: 'identity-provider.saml-config.tooltip' | translate}} diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html index 5897f9972b..5090374515 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html @@ -97,6 +97,18 @@ {{:: 'first-broker-login-flow.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'post-broker-login-flow.tooltip' | translate}} +
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 862f7239a9..2f7e9af2fc 100755 --- a/model/api/src/main/java/org/keycloak/models/IdentityProviderModel.java +++ b/model/api/src/main/java/org/keycloak/models/IdentityProviderModel.java @@ -58,6 +58,8 @@ public class IdentityProviderModel implements Serializable { private String firstBrokerLoginFlowId; + private String postBrokerLoginFlowId; + /** *

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.

@@ -78,6 +80,7 @@ public class IdentityProviderModel implements Serializable { this.authenticateByDefault = model.isAuthenticateByDefault(); this.addReadTokenRoleOnCreate = model.addReadTokenRoleOnCreate; this.firstBrokerLoginFlowId = model.getFirstBrokerLoginFlowId(); + this.postBrokerLoginFlowId = model.getPostBrokerLoginFlowId(); } public String getInternalId() { @@ -136,6 +139,14 @@ public class IdentityProviderModel implements Serializable { this.firstBrokerLoginFlowId = firstBrokerLoginFlowId; } + public String getPostBrokerLoginFlowId() { + return postBrokerLoginFlowId; + } + + public void setPostBrokerLoginFlowId(String postBrokerLoginFlowId) { + this.postBrokerLoginFlowId = postBrokerLoginFlowId; + } + public Map getConfig() { return this.config; } 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 f92f1d29c0..54a47bc893 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/IdentityProviderEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/IdentityProviderEntity.java @@ -35,6 +35,7 @@ public class IdentityProviderEntity { protected boolean addReadTokenRoleOnCreate; private boolean authenticateByDefault; private String firstBrokerLoginFlowId; + private String postBrokerLoginFlowId; private Map config = new HashMap(); @@ -78,6 +79,14 @@ public class IdentityProviderEntity { this.firstBrokerLoginFlowId = firstBrokerLoginFlowId; } + public String getPostBrokerLoginFlowId() { + return postBrokerLoginFlowId; + } + + public void setPostBrokerLoginFlowId(String postBrokerLoginFlowId) { + this.postBrokerLoginFlowId = postBrokerLoginFlowId; + } + public boolean isStoreToken() { return this.storeToken; } 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 e60d915811..1d34068971 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 @@ -512,6 +512,15 @@ public class ModelToRepresentation { providerRep.setFirstBrokerLoginFlowAlias(flow.getAlias()); } + String postBrokerLoginFlowId = identityProviderModel.getPostBrokerLoginFlowId(); + if (postBrokerLoginFlowId != null) { + AuthenticationFlowModel flow = realm.getAuthenticationFlowById(postBrokerLoginFlowId); + if (flow == null) { + throw new ModelException("Couldn't find authentication flow with id " + postBrokerLoginFlowId); + } + providerRep.setPostBrokerLoginFlowAlias(flow.getAlias()); + } + return providerRep; } 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 87c13a3e08..53284884b8 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 @@ -1204,6 +1204,17 @@ public class RepresentationToModel { } identityProviderModel.setFirstBrokerLoginFlowId(flowModel.getId()); + flowAlias = representation.getPostBrokerLoginFlowAlias(); + if (flowAlias == null || flowAlias.trim().length() == 0) { + identityProviderModel.setPostBrokerLoginFlowId(null); + } else { + flowModel = realm.getFlowByAlias(flowAlias); + if (flowModel == null) { + throw new ModelException("No available authentication flow with alias: " + flowAlias); + } + identityProviderModel.setPostBrokerLoginFlowId(flowModel.getId()); + } + return identityProviderModel; } 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 3f5817956b..cfd7347ae9 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 @@ -1277,6 +1277,7 @@ public class RealmAdapter implements RealmModel { identityProviderModel.setTrustEmail(entity.isTrustEmail()); identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault()); identityProviderModel.setFirstBrokerLoginFlowId(entity.getFirstBrokerLoginFlowId()); + identityProviderModel.setPostBrokerLoginFlowId(entity.getPostBrokerLoginFlowId()); identityProviderModel.setStoreToken(entity.isStoreToken()); identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate()); @@ -1310,6 +1311,7 @@ public class RealmAdapter implements RealmModel { entity.setTrustEmail(identityProvider.isTrustEmail()); entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault()); entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId()); + entity.setPostBrokerLoginFlowId(identityProvider.getPostBrokerLoginFlowId()); entity.setConfig(identityProvider.getConfig()); realm.addIdentityProvider(entity); @@ -1337,6 +1339,7 @@ public class RealmAdapter implements RealmModel { entity.setTrustEmail(identityProvider.isTrustEmail()); entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault()); entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId()); + entity.setPostBrokerLoginFlowId(identityProvider.getPostBrokerLoginFlowId()); entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate()); entity.setStoreToken(identityProvider.isStoreToken()); entity.setConfig(identityProvider.getConfig()); 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 6bbc31c614..2101de21f4 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 @@ -57,6 +57,9 @@ public class IdentityProviderEntity { @Column(name="FIRST_BROKER_LOGIN_FLOW_ID") private String firstBrokerLoginFlowId; + @Column(name="POST_BROKER_LOGIN_FLOW_ID") + private String postBrokerLoginFlowId; + @ElementCollection @MapKeyColumn(name="NAME") @Column(name="VALUE", columnDefinition = "TEXT") @@ -127,6 +130,14 @@ public class IdentityProviderEntity { this.firstBrokerLoginFlowId = firstBrokerLoginFlowId; } + public String getPostBrokerLoginFlowId() { + return postBrokerLoginFlowId; + } + + public void setPostBrokerLoginFlowId(String postBrokerLoginFlowId) { + this.postBrokerLoginFlowId = postBrokerLoginFlowId; + } + public Map getConfig() { return this.config; } 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 fc840ba534..4229cd97a2 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 @@ -949,6 +949,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme identityProviderModel.setTrustEmail(entity.isTrustEmail()); identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault()); identityProviderModel.setFirstBrokerLoginFlowId(entity.getFirstBrokerLoginFlowId()); + identityProviderModel.setPostBrokerLoginFlowId(entity.getPostBrokerLoginFlowId()); identityProviderModel.setStoreToken(entity.isStoreToken()); identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate()); @@ -982,6 +983,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme entity.setStoreToken(identityProvider.isStoreToken()); entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault()); entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId()); + entity.setPostBrokerLoginFlowId(identityProvider.getPostBrokerLoginFlowId()); entity.setConfig(identityProvider.getConfig()); realm.getIdentityProviders().add(entity); @@ -1008,6 +1010,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme entity.setTrustEmail(identityProvider.isTrustEmail()); entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault()); entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId()); + entity.setPostBrokerLoginFlowId(identityProvider.getPostBrokerLoginFlowId()); entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate()); entity.setStoreToken(identityProvider.isStoreToken()); entity.setConfig(identityProvider.getConfig()); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/AbstractIdpAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/AbstractIdpAuthenticator.java index b7c766398d..60376cd019 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/AbstractIdpAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/AbstractIdpAuthenticator.java @@ -44,7 +44,7 @@ public abstract class AbstractIdpAuthenticator implements Authenticator { public void authenticate(AuthenticationFlowContext context) { ClientSessionModel clientSession = context.getClientSession(); - SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession); + SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, BROKERED_CONTEXT_NOTE); if (serializedCtx == null) { throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR); } @@ -61,7 +61,7 @@ public abstract class AbstractIdpAuthenticator implements Authenticator { public void action(AuthenticationFlowContext context) { ClientSessionModel clientSession = context.getClientSession(); - SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession); + SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, BROKERED_CONTEXT_NOTE); if (serializedCtx == null) { throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java index a4d0ea345c..aa7ed77d18 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java @@ -111,7 +111,7 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator { AttributeFormDataProcessor.process(formData, realm, userCtx); - userCtx.saveToClientSession(context.getClientSession()); + userCtx.saveToClientSession(context.getClientSession(), BROKERED_CONTEXT_NOTE); logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername()); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java index 5e732d8be7..616a3c4405 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java @@ -41,7 +41,7 @@ public class IdpUsernamePasswordForm extends UsernamePasswordForm { } protected LoginFormsProvider setupForm(AuthenticationFlowContext context, MultivaluedMap formData, UserModel existingUser) { - SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(context.getClientSession()); + SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(context.getClientSession(), AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); if (serializedCtx == null) { throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/PostBrokerLoginConstants.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/PostBrokerLoginConstants.java new file mode 100644 index 0000000000..a7d9667232 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/PostBrokerLoginConstants.java @@ -0,0 +1,18 @@ +package org.keycloak.authentication.authenticators.broker.util; + +/** + * @author Marek Posolda + */ +public interface PostBrokerLoginConstants { + + // ClientSession note with serialized BrokeredIdentityContext used during postBrokerLogin flow + String PBL_BROKERED_IDENTITY_CONTEXT = "PBL_BROKERED_IDENTITY_CONTEXT"; + + // ClientSession note flag specifying if postBrokerLogin flow was triggered after 1st login with this broker after firstBrokerLogin flow is finished (true) + // or after 2nd or more login with this broker (false) + String PBL_AFTER_FIRST_BROKER_LOGIN = "PBL_AFTER_FIRST_BROKER_LOGIN"; + + // Prefix for the clientSession note key (suffix will be identityProvider alias, so the whole note key will be something like PBL_AUTH_STATE.facebook ) + // It holds the flag whether PostBrokerLogin flow for specified broker was successfully executed for this clientSession + String PBL_AUTH_STATE_PREFIX = "PBL_AUTH_STATE."; +} diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java index 968831c1b4..2edbf6eea8 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java @@ -301,17 +301,17 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext { } // Save this context as note to clientSession - public void saveToClientSession(ClientSessionModel clientSession) { + public void saveToClientSession(ClientSessionModel clientSession, String noteKey) { try { String asString = JsonSerialization.writeValueAsString(this); - clientSession.setNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE, asString); + clientSession.setNote(noteKey, asString); } catch (IOException ioe) { throw new RuntimeException(ioe); } } - public static SerializedBrokeredIdentityContext readFromClientSession(ClientSessionModel clientSession) { - String asString = clientSession.getNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); + public static SerializedBrokeredIdentityContext readFromClientSession(ClientSessionModel clientSession, String noteKey) { + String asString = clientSession.getNote(noteKey); if (asString == null) { return null; } else { diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java index 51c61827de..f41ee70a0e 100755 --- a/services/src/main/java/org/keycloak/services/Urls.java +++ b/services/src/main/java/org/keycloak/services/Urls.java @@ -103,6 +103,13 @@ public class Urls { .build(realmName); } + public static URI identityProviderAfterPostBrokerLogin(URI baseUri, String realmName, String accessCode) { + return realmBase(baseUri).path(RealmsResource.class, "getBrokerService") + .path(IdentityBrokerService.class, "afterPostBrokerLoginFlow") + .replaceQueryParam(OAuth2Constants.CODE, accessCode) + .build(realmName); + } + public static URI accountTotpPage(URI baseUri, String realmId) { return accountBase(baseUri).path(AccountService.class, "totpPage").build(realmId); } diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index 89f8c394f6..eb3f8167a2 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -22,6 +22,7 @@ import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.OAuth2Constants; import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator; +import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants; import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext; import org.keycloak.common.ClientConnection; import org.keycloak.authentication.AuthenticationProcessor; @@ -33,7 +34,6 @@ import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.broker.provider.IdentityProviderMapper; import org.keycloak.common.util.Time; import org.keycloak.events.Details; -import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.login.LoginFormsProvider; @@ -310,7 +310,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal clientSession.setTimestamp(Time.currentTime()); SerializedBrokeredIdentityContext ctx = SerializedBrokeredIdentityContext.serialize(context); - ctx.saveToClientSession(clientSession); + ctx.saveToClientSession(clientSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); URI redirect = LoginActionsService.firstBrokerLoginProcessor(uriInfo) .queryParam(OAuth2Constants.CODE, context.getCode()) @@ -319,21 +319,9 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } else { updateFederatedIdentity(context, federatedUser); + clientSession.setAuthenticatedUser(federatedUser); - boolean firstBrokerLoginInProgress = (clientSession.getNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null); - if (firstBrokerLoginInProgress) { - LOGGER.debugf("Reauthenticated with broker '%s' when linking user '%s' with other broker", context.getIdpConfig().getAlias(), federatedUser.getUsername()); - - UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realmModel, clientSession); - if (!linkingUser.getId().equals(federatedUser.getId())) { - return redirectToErrorPage(Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE, federatedUser.getUsername(), linkingUser.getUsername()); - } - - clientSession.setAuthenticatedUser(federatedUser); - return afterFirstBrokerLogin(context.getCode()); - } - - return finishBrokerAuthentication(context, federatedUser, clientSession, providerId); + return finishOrRedirectToPostBrokerLogin(clientSession, context, false); } } @@ -345,13 +333,19 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal ClientSessionModel clientSession = clientCode.getClientSession(); try { - SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession); + this.event.detail(Details.CODE_ID, clientSession.getId()) + .removeDetail("auth_method"); + + SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); if (serializedCtx == null) { throw new IdentityBrokerException("Not found serialized context in clientSession"); } BrokeredIdentityContext context = serializedCtx.deserialize(session, clientSession); String providerId = context.getIdpConfig().getAlias(); + event.detail(Details.IDENTITY_PROVIDER, providerId); + event.detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername()); + // firstBrokerLogin workflow finished. Removing note now clientSession.removeNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); @@ -360,8 +354,15 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal throw new IdentityBrokerException("Couldn't found authenticated federatedUser in clientSession"); } + event.user(federatedUser); + event.detail(Details.USERNAME, federatedUser.getUsername()); + if (context.getIdpConfig().isAddReadTokenRoleOnCreate()) { - RoleModel readTokenRole = realmModel.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE); + ClientModel brokerClient = realmModel.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID); + if (brokerClient == null) { + throw new IdentityBrokerException("Client 'broker' not available. Maybe realm has not migrated to support the broker token exchange service"); + } + RoleModel readTokenRole = brokerClient.getRole(Constants.READ_TOKEN_ROLE); federatedUser.grantRole(readTokenRole); } @@ -370,12 +371,6 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal context.getUsername(), context.getToken()); session.users().addFederatedIdentity(realmModel, federatedUser, federatedIdentityModel); - EventBuilder event = this.event.clone().user(federatedUser) - .detail(Details.CODE_ID, clientSession.getId()) - .detail(Details.USERNAME, federatedUser.getUsername()) - .detail(Details.IDENTITY_PROVIDER, providerId) - .detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername()) - .removeDetail("auth_method"); String isRegisteredNewUser = clientSession.getNote(AbstractIdpAuthenticator.BROKER_REGISTERED_NEW_USER); if (Boolean.parseBoolean(isRegisteredNewUser)) { @@ -411,20 +406,108 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal updateFederatedIdentity(context, federatedUser); } - String isDifferentBrowser = clientSession.getNote(AbstractIdpAuthenticator.IS_DIFFERENT_BROWSER); - if (Boolean.parseBoolean(isDifferentBrowser)) { - session.sessions().removeClientSession(realmModel, clientSession); - return session.getProvider(LoginFormsProvider.class) - .setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, context.getIdpConfig().getAlias(), context.getUsername()) - .createInfoPage(); - } else { - return finishBrokerAuthentication(context, federatedUser, clientSession, providerId); - } + return finishOrRedirectToPostBrokerLogin(clientSession, context, true); + } catch (Exception e) { return redirectToErrorPage(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, e); } } + + private Response finishOrRedirectToPostBrokerLogin(ClientSessionModel clientSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin) { + String postBrokerLoginFlowId = context.getIdpConfig().getPostBrokerLoginFlowId(); + if (postBrokerLoginFlowId == null) { + + LOGGER.debugf("Skip redirect to postBrokerLogin flow. PostBrokerLogin flow not set for identityProvider '%s'.", context.getIdpConfig().getAlias()); + return afterPostBrokerLoginFlowSuccess(clientSession, context, wasFirstBrokerLogin); + } else { + + LOGGER.debugf("Redirect to postBrokerLogin flow after authentication with identityProvider '%s'.", context.getIdpConfig().getAlias()); + + clientSession.setTimestamp(Time.currentTime()); + + SerializedBrokeredIdentityContext ctx = SerializedBrokeredIdentityContext.serialize(context); + ctx.saveToClientSession(clientSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT); + + clientSession.setNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN, String.valueOf(wasFirstBrokerLogin)); + + URI redirect = LoginActionsService.postBrokerLoginProcessor(uriInfo) + .queryParam(OAuth2Constants.CODE, context.getCode()) + .build(realmModel.getName()); + return Response.status(302).location(redirect).build(); + } + } + + + // Callback from LoginActionsService after postBrokerLogin flow is finished + @GET + @Path("/after-post-broker-login") + public Response afterPostBrokerLoginFlow(@QueryParam("code") String code) { + ClientSessionCode clientCode = parseClientSessionCode(code); + ClientSessionModel clientSession = clientCode.getClientSession(); + + try { + SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT); + if (serializedCtx == null) { + throw new IdentityBrokerException("Not found serialized context in clientSession. Note " + PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT + " was null"); + } + BrokeredIdentityContext context = serializedCtx.deserialize(session, clientSession); + + String wasFirstBrokerLoginNote = clientSession.getNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN); + boolean wasFirstBrokerLogin = Boolean.parseBoolean(wasFirstBrokerLoginNote); + + // Ensure the post-broker-login flow was successfully finished + String authStateNoteKey = PostBrokerLoginConstants.PBL_AUTH_STATE_PREFIX + context.getIdpConfig().getAlias(); + String authState = clientSession.getNote(authStateNoteKey); + if (!Boolean.parseBoolean(authState)) { + throw new IdentityBrokerException("Invalid request. Not found the flag that post-broker-login flow was finished"); + } + + // remove notes + clientSession.removeNote(PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT); + clientSession.removeNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN); + + return afterPostBrokerLoginFlowSuccess(clientSession, context, wasFirstBrokerLogin); + } catch (IdentityBrokerException e) { + return redirectToErrorPage(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, e); + } + } + + private Response afterPostBrokerLoginFlowSuccess(ClientSessionModel clientSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin) { + String providerId = context.getIdpConfig().getAlias(); + UserModel federatedUser = clientSession.getAuthenticatedUser(); + + if (wasFirstBrokerLogin) { + + String isDifferentBrowser = clientSession.getNote(AbstractIdpAuthenticator.IS_DIFFERENT_BROWSER); + if (Boolean.parseBoolean(isDifferentBrowser)) { + session.sessions().removeClientSession(realmModel, clientSession); + return session.getProvider(LoginFormsProvider.class) + .setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, context.getIdpConfig().getAlias(), context.getUsername()) + .createInfoPage(); + } else { + return finishBrokerAuthentication(context, federatedUser, clientSession, providerId); + } + + } else { + + boolean firstBrokerLoginInProgress = (clientSession.getNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null); + if (firstBrokerLoginInProgress) { + LOGGER.debugf("Reauthenticated with broker '%s' when linking user '%s' with other broker", context.getIdpConfig().getAlias(), federatedUser.getUsername()); + + UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realmModel, clientSession); + if (!linkingUser.getId().equals(federatedUser.getId())) { + return redirectToErrorPage(Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE, federatedUser.getUsername(), linkingUser.getUsername()); + } + + return afterFirstBrokerLogin(context.getCode()); + } else { + return finishBrokerAuthentication(context, federatedUser, clientSession, providerId); + } + } + } + + private Response finishBrokerAuthentication(BrokeredIdentityContext context, UserModel federatedUser, ClientSessionModel clientSession, String providerId) { UserSessionModel userSession = this.session.sessions() .createUserSession(this.realmModel, federatedUser, federatedUser.getUsername(), this.clientConnection.getRemoteAddr(), "broker", false, context.getBrokerSessionId(), context.getBrokerUserId()); @@ -443,6 +526,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal return AuthenticationProcessor.createRequiredActionRedirect(realmModel, clientSession, uriInfo); } + @Override public Response cancelled(String code) { ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realmModel); diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index 36e4c6c1d6..75dd87ee8b 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -24,6 +24,7 @@ package org.keycloak.services.resources; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator; +import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants; import org.keycloak.authentication.requiredactions.VerifyEmail; import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext; @@ -98,6 +99,7 @@ public class LoginActionsService { public static final String RESET_CREDENTIALS_PATH = "reset-credentials"; public static final String REQUIRED_ACTION = "required-action"; public static final String FIRST_BROKER_LOGIN_PATH = "first-broker-login"; + public static final String POST_BROKER_LOGIN_PATH = "post-broker-login"; private RealmModel realm; @@ -144,6 +146,10 @@ public class LoginActionsService { return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "firstBrokerLoginGet"); } + public static UriBuilder postBrokerLoginProcessor(UriInfo uriInfo) { + return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "postBrokerLoginGet"); + } + public static UriBuilder loginActionsBaseUrl(UriBuilder baseUriBuilder) { return baseUriBuilder.path(RealmsResource.class).path(RealmsResource.class, "getLoginActionsService"); } @@ -407,7 +413,7 @@ public class LoginActionsService { logger.debugf("Forget-password flow finished when authenticated user '%s' after first broker login.", linkingUser.getUsername()); - return redirectToAfterFirstBrokerLoginEndpoint(clientSession); + return redirectToAfterBrokerLoginEndpoint(clientSession, true); } else { return super.authenticationComplete(); } @@ -480,22 +486,39 @@ public class LoginActionsService { return processRegistration(execution, clientSession, null); } + @Path(FIRST_BROKER_LOGIN_PATH) @GET public Response firstBrokerLoginGet(@QueryParam("code") String code, @QueryParam("execution") String execution) { - return firstBrokerLogin(code, execution); + return brokerLoginFlow(code, execution, true); } @Path(FIRST_BROKER_LOGIN_PATH) @POST public Response firstBrokerLoginPost(@QueryParam("code") String code, @QueryParam("execution") String execution) { - return firstBrokerLogin(code, execution); + return brokerLoginFlow(code, execution, true); } - protected Response firstBrokerLogin(String code, String execution) { - event.event(EventType.IDENTITY_PROVIDER_FIRST_LOGIN); + @Path(POST_BROKER_LOGIN_PATH) + @GET + public Response postBrokerLoginGet(@QueryParam("code") String code, + @QueryParam("execution") String execution) { + return brokerLoginFlow(code, execution, false); + } + + @Path(POST_BROKER_LOGIN_PATH) + @POST + public Response postBrokerLoginPost(@QueryParam("code") String code, + @QueryParam("execution") String execution) { + return brokerLoginFlow(code, execution, false); + } + + + protected Response brokerLoginFlow(String code, String execution, final boolean firstBrokerLogin) { + EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN; + event.event(eventType); Checks checks = new Checks(); if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { @@ -503,16 +526,29 @@ public class LoginActionsService { } event.detail(Details.CODE_ID, code); ClientSessionCode clientSessionCode = checks.clientCode; - ClientSessionModel clientSession = clientSessionCode.getClientSession(); + final ClientSessionModel clientSessionn = clientSessionCode.getClientSession(); - SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession); + String noteKey = firstBrokerLogin ? AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE : PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT; + SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSessionn, noteKey); if (serializedCtx == null) { - throw new WebApplicationException(ErrorPage.error(session, "Not found serialized context in clientSession")); + logger.errorf("Not found serialized context in clientSession under note '%s'", noteKey); + throw new WebApplicationException(ErrorPage.error(session, "Not found serialized context in clientSession.")); } - BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, clientSession); - AuthenticationFlowModel firstBrokerLoginFlow = realm.getAuthenticationFlowById(brokerContext.getIdpConfig().getFirstBrokerLoginFlowId()); + BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, clientSessionn); + final String identityProviderAlias = brokerContext.getIdpConfig().getAlias(); - event.detail(Details.IDENTITY_PROVIDER, brokerContext.getIdpConfig().getAlias()) + String flowId = firstBrokerLogin ? brokerContext.getIdpConfig().getFirstBrokerLoginFlowId() : brokerContext.getIdpConfig().getPostBrokerLoginFlowId(); + if (flowId == null) { + logger.errorf("Flow not configured for identity provider '%s'", identityProviderAlias); + throw new WebApplicationException(ErrorPage.error(session, "Flow not configured for identity provider")); + } + AuthenticationFlowModel brokerLoginFlow = realm.getAuthenticationFlowById(flowId); + if (brokerLoginFlow == null) { + logger.errorf("Not found configured flow with ID '%s' for identity provider '%s'", flowId, identityProviderAlias); + throw new WebApplicationException(ErrorPage.error(session, "Flow not found for identity provider")); + } + + event.detail(Details.IDENTITY_PROVIDER, identityProviderAlias) .detail(Details.IDENTITY_PROVIDER_USERNAME, brokerContext.getUsername()); @@ -520,19 +556,26 @@ public class LoginActionsService { @Override protected Response authenticationComplete() { - return redirectToAfterFirstBrokerLoginEndpoint(clientSession); + if (!firstBrokerLogin) { + String authStateNoteKey = PostBrokerLoginConstants.PBL_AUTH_STATE_PREFIX + identityProviderAlias; + clientSessionn.setNote(authStateNoteKey, "true"); + } + + return redirectToAfterBrokerLoginEndpoint(clientSession, firstBrokerLogin); } }; - return processFlow(execution, clientSession, FIRST_BROKER_LOGIN_PATH, firstBrokerLoginFlow, null, processor); + String flowPath = firstBrokerLogin ? FIRST_BROKER_LOGIN_PATH : POST_BROKER_LOGIN_PATH; + return processFlow(execution, clientSessionn, flowPath, brokerLoginFlow, null, processor); } - private Response redirectToAfterFirstBrokerLoginEndpoint(ClientSessionModel clientSession) { + private Response redirectToAfterBrokerLoginEndpoint(ClientSessionModel clientSession, boolean firstBrokerLogin) { ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession); clientSession.setTimestamp(Time.currentTime()); - URI redirect = Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()); + URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()) : + Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()) ; logger.debugf("Redirecting to '%s' ", redirect); return Response.status(302).location(redirect).build(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java index 36bf4c7908..16ad0e32c9 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java @@ -422,7 +422,7 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi } - protected void setExecutionRequirement(RealmModel realmWithBroker, String flowAlias, String authenticatorProvider, AuthenticationExecutionModel.Requirement requirement) { + protected static void setExecutionRequirement(RealmModel realmWithBroker, String flowAlias, String authenticatorProvider, AuthenticationExecutionModel.Requirement requirement) { AuthenticationFlowModel flowModel = realmWithBroker.getFlowByAlias(flowAlias); List authExecutions = realmWithBroker.getAuthenticationExecutions(flowModel.getId()); for (AuthenticationExecutionModel execution : authExecutions) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java index 7545a5069c..1c3e043ca7 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -261,7 +261,7 @@ public abstract class AbstractIdentityProviderTest { return getRealm(this.session); } - protected RealmModel getRealm(KeycloakSession session) { + protected static RealmModel getRealm(KeycloakSession session) { return session.realms().getRealm("realm-with-broker"); } @@ -312,7 +312,7 @@ public abstract class AbstractIdentityProviderTest { }); } - protected void setUpdateProfileFirstLogin(RealmModel realm, String updateProfileFirstLogin) { + protected static void setUpdateProfileFirstLogin(RealmModel realm, String updateProfileFirstLogin) { AuthenticatorConfigModel reviewProfileConfig = realm.getAuthenticatorConfigByAlias(DefaultAuthenticationFlows.IDP_REVIEW_PROFILE_CONFIG_ALIAS); reviewProfileConfig.getConfig().put(IdpReviewProfileAuthenticatorFactory.UPDATE_PROFILE_ON_FIRST_LOGIN, updateProfileFirstLogin); realm.updateAuthenticatorConfig(reviewProfileConfig); 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 8e23f114d2..4dd34d78d2 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java @@ -22,6 +22,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import org.junit.Assert; import org.junit.Test; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.oidc.OIDCIdentityProvider; @@ -85,6 +86,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes identityProviderModel.setStoreToken(true); identityProviderModel.setAuthenticateByDefault(true); identityProviderModel.setFirstBrokerLoginFlowId(realm.getBrowserFlow().getId()); + identityProviderModel.setPostBrokerLoginFlowId(realm.getDirectGrantFlow().getId()); realm.updateIdentityProvider(identityProviderModel); @@ -100,6 +102,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes assertTrue(identityProviderModel.isStoreToken()); assertTrue(identityProviderModel.isAuthenticateByDefault()); assertEquals(identityProviderModel.getFirstBrokerLoginFlowId(), realm.getBrowserFlow().getId()); + assertEquals(identityProviderModel.getPostBrokerLoginFlowId(), realm.getDirectGrantFlow().getId()); identityProviderModel.getConfig().remove("config-added"); identityProviderModel.setEnabled(true); @@ -221,6 +224,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes assertEquals("clientId", config.getClientId()); assertEquals("clientSecret", config.getClientSecret()); assertEquals(realm.getBrowserFlow().getId(), identityProvider.getFirstBrokerLoginFlowId()); + Assert.assertNull(identityProvider.getPostBrokerLoginFlowId()); assertEquals(FacebookIdentityProvider.AUTH_URL, config.getAuthorizationUrl()); assertEquals(FacebookIdentityProvider.TOKEN_URL, config.getTokenUrl()); assertEquals(FacebookIdentityProvider.PROFILE_URL, config.getUserInfoUrl()); @@ -239,6 +243,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes assertEquals("clientId", config.getClientId()); assertEquals("clientSecret", config.getClientSecret()); assertEquals(realm.getFlowByAlias(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW).getId(), identityProvider.getFirstBrokerLoginFlowId()); + assertEquals(realm.getBrowserFlow().getId(), identityProvider.getPostBrokerLoginFlowId()); assertEquals(GitHubIdentityProvider.AUTH_URL, config.getAuthorizationUrl()); assertEquals(GitHubIdentityProvider.TOKEN_URL, config.getTokenUrl()); assertEquals(GitHubIdentityProvider.PROFILE_URL, config.getUserInfoUrl()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/PostBrokerFlowTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/PostBrokerFlowTest.java new file mode 100644 index 0000000000..dc8cc35257 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/PostBrokerFlowTest.java @@ -0,0 +1,302 @@ +package org.keycloak.testsuite.broker; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.DefaultAuthenticationFlows; +import org.keycloak.models.utils.TimeBasedOTP; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.KeycloakServer; +import org.keycloak.testsuite.pages.IdpConfirmLinkPage; +import org.keycloak.testsuite.pages.LoginConfigTotpPage; +import org.keycloak.testsuite.pages.LoginTotpPage; +import org.keycloak.testsuite.rule.AbstractKeycloakRule; +import org.keycloak.testsuite.rule.WebResource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Marek Posolda + */ +public class PostBrokerFlowTest extends AbstractIdentityProviderTest { + + private static final int PORT = 8082; + + private static String POST_BROKER_FLOW_ID; + + private static final String APP_REALM_ID = "realm-with-broker"; + + @ClassRule + public static AbstractKeycloakRule samlServerRule = new AbstractKeycloakRule() { + + @Override + protected void configureServer(KeycloakServer server) { + server.getConfig().setPort(PORT); + } + + @Override + protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) { + server.importRealm(getClass().getResourceAsStream("/broker-test/test-broker-realm-with-kc-oidc.json")); + server.importRealm(getClass().getResourceAsStream("/broker-test/test-broker-realm-with-saml.json")); + + RealmModel realmWithBroker = getRealm(session); + + // Disable "idp-email-verification" authenticator in firstBrokerLogin flow. Disable updateProfileOnFirstLogin page + AbstractFirstBrokerLoginTest.setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_HANDLE_EXISTING_SUBFLOW, + IdpEmailVerificationAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.DISABLED); + setUpdateProfileFirstLogin(realmWithBroker, IdentityProviderRepresentation.UPFLM_OFF); + + // Add post-broker flow with OTP authenticator to the realm + AuthenticationFlowModel postBrokerFlow = new AuthenticationFlowModel(); + postBrokerFlow.setAlias("post-broker"); + postBrokerFlow.setDescription("post-broker flow with OTP"); + postBrokerFlow.setProviderId("basic-flow"); + postBrokerFlow.setTopLevel(true); + postBrokerFlow.setBuiltIn(false); + postBrokerFlow = realmWithBroker.addAuthenticationFlow(postBrokerFlow); + + POST_BROKER_FLOW_ID = postBrokerFlow.getId(); + + AuthenticationExecutionModel execution = new AuthenticationExecutionModel(); + execution.setParentFlow(postBrokerFlow.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator("auth-otp-form"); + execution.setPriority(20); + execution.setAuthenticatorFlow(false); + realmWithBroker.addAuthenticatorExecution(execution); + + } + + @Override + protected String[] getTestRealms() { + return new String[] { "realm-with-oidc-identity-provider", "realm-with-saml-idp-basic" }; + } + }; + + + @WebResource + protected IdpConfirmLinkPage idpConfirmLinkPage; + + @WebResource + protected LoginTotpPage loginTotpPage; + + @WebResource + protected LoginConfigTotpPage totpPage; + + private TimeBasedOTP totp = new TimeBasedOTP(); + + + @Override + protected String getProviderId() { + return "kc-oidc-idp"; + } + + + @Test + public void testPostBrokerLoginWithOTP() { + // enable post-broker flow + IdentityProviderModel identityProvider = getIdentityProviderModel(); + setPostBrokerFlowForProvider(identityProvider, getRealm(), true); + + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + // login with broker and assert that OTP needs to be set. + loginIDP("test-user"); + totpPage.assertCurrent(); + String totpSecret = totpPage.getTotpSecret(); + totpPage.configure(totp.generateTOTP(totpSecret)); + + assertFederatedUser(getProviderId() + ".test-user", "test-user@localhost", "test-user", getProviderId()); + + driver.navigate().to("http://localhost:8081/test-app/logout"); + + // Login again and assert that OTP needs to be provided. + loginIDP("test-user"); + loginTotpPage.assertCurrent(); + loginTotpPage.login(totp.generateTOTP(totpSecret)); + + assertFederatedUser(getProviderId() + ".test-user", "test-user@localhost", "test-user", getProviderId()); + + driver.navigate().to("http://localhost:8081/test-app/logout"); + + // Disable post-broker and ensure that OTP is not required anymore + setPostBrokerFlowForProvider(identityProvider, getRealm(), false); + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + loginIDP("test-user"); + assertFederatedUser(getProviderId() + ".test-user", "test-user@localhost", "test-user", getProviderId()); + driver.navigate().to("http://localhost:8081/test-app/logout"); + } + + + @Test + public void testBrokerReauthentication_samlBrokerWithOTPRequired() throws Exception { + RealmModel realmWithBroker = getRealm(); + + // Enable OTP just for SAML provider + IdentityProviderModel samlIdentityProvider = realmWithBroker.getIdentityProviderByAlias("kc-saml-idp-basic"); + setPostBrokerFlowForProvider(samlIdentityProvider, realmWithBroker, true); + + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + // ensure TOTP setup is required during SAML broker firstLogin and during reauthentication for link OIDC broker too + reauthenticateOIDCWithSAMLBroker(true, false); + + // Disable TOTP for SAML provider + realmWithBroker = getRealm(); + samlIdentityProvider = realmWithBroker.getIdentityProviderByAlias("kc-saml-idp-basic"); + setPostBrokerFlowForProvider(samlIdentityProvider, realmWithBroker, false); + + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + } + + @Test + public void testBrokerReauthentication_oidcBrokerWithOTPRequired() throws Exception { + + // Enable OTP just for OIDC provider + IdentityProviderModel oidcIdentityProvider = getIdentityProviderModel(); + setPostBrokerFlowForProvider(oidcIdentityProvider, getRealm(), true); + + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + // ensure TOTP setup is not required during SAML broker firstLogin, but during reauthentication for link OIDC broker + reauthenticateOIDCWithSAMLBroker(false, true); + + // Disable TOTP for SAML provider + oidcIdentityProvider = getIdentityProviderModel(); + setPostBrokerFlowForProvider(oidcIdentityProvider, getRealm(), false); + + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + } + + @Test + public void testBrokerReauthentication_bothBrokerWithOTPRequired() throws Exception { + RealmModel realmWithBroker = getRealm(); + + // Enable OTP for both OIDC and SAML provider + IdentityProviderModel samlIdentityProvider = realmWithBroker.getIdentityProviderByAlias("kc-saml-idp-basic"); + setPostBrokerFlowForProvider(samlIdentityProvider, realmWithBroker, true); + + IdentityProviderModel oidcIdentityProvider = getIdentityProviderModel(); + setPostBrokerFlowForProvider(oidcIdentityProvider, getRealm(), true); + + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + // ensure TOTP setup is required during SAML broker firstLogin and during reauthentication for link OIDC broker too + reauthenticateOIDCWithSAMLBroker(true, true); + + // Disable TOTP for both SAML and OIDC provider + realmWithBroker = getRealm(); + samlIdentityProvider = realmWithBroker.getIdentityProviderByAlias("kc-saml-idp-basic"); + setPostBrokerFlowForProvider(samlIdentityProvider, realmWithBroker, false); + + oidcIdentityProvider = getIdentityProviderModel(); + setPostBrokerFlowForProvider(oidcIdentityProvider, getRealm(), false); + + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + } + + + private void reauthenticateOIDCWithSAMLBroker(boolean samlBrokerTotpEnabled, boolean oidcBrokerTotpEnabled) { + // First login as "testuser" with SAML broker + driver.navigate().to("http://localhost:8081/test-app"); + this.loginPage.clickSocial("kc-saml-idp-basic"); + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/")); + Assert.assertEquals("Log in to realm-with-saml-idp-basic", this.driver.getTitle()); + this.loginPage.login("test-user", "password"); + + // Ensure user needs to setup TOTP if SAML broker requires that + String totpSecret = null; + if (samlBrokerTotpEnabled) { + totpPage.assertCurrent(); + totpSecret = totpPage.getTotpSecret(); + totpPage.configure(totp.generateTOTP(totpSecret)); + } + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); + driver.navigate().to("http://localhost:8081/test-app/logout"); + + // login through OIDC broker now + loginIDP("test-user"); + + this.idpConfirmLinkPage.assertCurrent(); + Assert.assertEquals("User with email test-user@localhost already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); + this.idpConfirmLinkPage.clickLinkAccount(); + + // assert reauthentication with login page. On login page is link to kc-saml-idp-basic as user has it linked already + Assert.assertEquals("Log in to " + APP_REALM_ID, this.driver.getTitle()); + Assert.assertEquals("Authenticate as kc-saml-idp-basic.test-user to link your account with " + getProviderId(), this.loginPage.getSuccessMessage()); + + // reauthenticate with SAML broker. OTP authentication is required as well + this.loginPage.clickSocial("kc-saml-idp-basic"); + Assert.assertEquals("Log in to realm-with-saml-idp-basic", this.driver.getTitle()); + this.loginPage.login("test-user", "password"); + + if (samlBrokerTotpEnabled) { + // User already set TOTP during first login with SAML broker + loginTotpPage.assertCurrent(); + loginTotpPage.login(totp.generateTOTP(totpSecret)); + } else if (oidcBrokerTotpEnabled) { + // User needs to set TOTP as first login with SAML broker didn't require that + totpPage.assertCurrent(); + totpSecret = totpPage.getTotpSecret(); + totpPage.configure(totp.generateTOTP(totpSecret)); + } + + // authenticated and redirected to app. User is linked with both identity providers + assertFederatedUser("kc-saml-idp-basic.test-user", "test-user@localhost", "test-user", getProviderId(), "kc-saml-idp-basic"); + } + + private void setPostBrokerFlowForProvider(IdentityProviderModel identityProvider, RealmModel realm, boolean enable) { + if (enable) { + identityProvider.setPostBrokerLoginFlowId(POST_BROKER_FLOW_ID); + } else { + identityProvider.setPostBrokerLoginFlowId(null); + } + realm.updateIdentityProvider(identityProvider); + } + + private void assertFederatedUser(String expectedUsername, String expectedEmail, String expectedFederatedUsername, String... expectedLinkedProviders) { + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); + UserModel federatedUser = getFederatedUser(); + + assertNotNull(federatedUser); + assertEquals(expectedUsername, federatedUser.getUsername()); + assertEquals(expectedEmail, federatedUser.getEmail()); + + RealmModel realmWithBroker = getRealm(); + Set federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realmWithBroker); + + List expectedProvidersList = Arrays.asList(expectedLinkedProviders); + assertEquals(expectedProvidersList.size(), federatedIdentities.size()); + for (FederatedIdentityModel federatedIdentityModel : federatedIdentities) { + String providerAlias = federatedIdentityModel.getIdentityProvider(); + Assert.assertTrue(expectedProvidersList.contains(providerAlias)); + assertEquals(expectedFederatedUsername, federatedIdentityModel.getUserName()); + } + } + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java index 232b102663..09782c91c1 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java @@ -61,7 +61,7 @@ public class LoginTotpPage extends AbstractPage { } public boolean isCurrent() { - if (driver.getTitle().equals("Log in to test")) { + if (driver.getTitle().startsWith("Log in to ")) { try { driver.findElement(By.id("totp")); return true; diff --git a/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json b/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json index 816aa49e02..28cc044e44 100755 --- a/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json +++ b/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json @@ -42,6 +42,7 @@ "providerId" : "github", "enabled": true, "storeToken": "false", + "postBrokerLoginFlowAlias" : "browser", "config": { "authorizationUrl": "authorizationUrl", "tokenUrl": "tokenUrl", From 17a2bd266bdb6455170a4daadcdb7e74d517a1cd Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 9 Dec 2015 17:34:44 +0100 Subject: [PATCH 2/2] KEYCLOAK-2124 post-broker login flow docs --- .../reference/en/en-US/modules/identity-broker.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/identity-broker.xml b/docbook/auth-server-docs/reference/en/en-US/modules/identity-broker.xml index 41c36f0c4f..cebf3af5b7 100755 --- a/docbook/auth-server-docs/reference/en/en-US/modules/identity-broker.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/identity-broker.xml @@ -332,6 +332,16 @@ More details in First Login section. + + + Post Login Flow + + Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user + authenticated with this identity provider (for example OTP). Leave this empty if you don't want any additional authenticators to be triggered after login + with this identity provider. Also note, that authenticator implementations must assume that user is already + set in ClientSession as identity provider already set it. + +