keycloak-uncached

Merge pull request #1936 from mposolda/master KEYCLOAK-2124

12/9/2015 2:35:00 PM

Changes

Details

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 0000000..c4dce2b
--- /dev/null
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
+    <changeSet author="mposolda@redhat.com" id="1.8.0">
+
+        <addColumn tableName="IDENTITY_PROVIDER">
+            <column name="POST_BROKER_LOGIN_FLOW_ID" type="VARCHAR(36)">
+                <constraints nullable="true"/>
+            </column>
+        </addColumn>
+
+    </changeSet>
+</databaseChangeLog>
\ 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 2acc0bb..0f907e0 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 @@
     <include file="META-INF/jpa-changelog-1.5.0.xml"/>
     <include file="META-INF/jpa-changelog-1.6.1.xml"/>
     <include file="META-INF/jpa-changelog-1.7.0.xml"/>
+    <include file="META-INF/jpa-changelog-1.8.0.xml"/>
 </databaseChangeLog>
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 0dc7f0a..c227abc 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<String, String> config = new HashMap<String, String>();
 
     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/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 41c36f0..cebf3af 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 <link linkend="identity-broker-first-login">First Login section</link>.
                         </entry>
                     </row>
+                    <row>
+                        <entry>
+                            <literal>Post Login Flow</literal>
+                        </entry>
+                        <entry>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.
+                        </entry>
+                    </row>
                 </tbody>
             </tgroup>
         </table>
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 b75728b..599dec5 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 0dd5168..287abe4 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 a66e205..445b8d0 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 4cbd12a..1bcf81b 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 @@
                 </div>
                 <kc-tooltip>{{:: 'first-broker-login-flow.tooltip' | translate}}</kc-tooltip>
             </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="postBrokerLoginFlowAlias">{{:: 'post-broker-login-flow' | translate}}</label>
+                <div class="col-md-6">
+                    <div>
+                        <select class="form-control" id="postBrokerLoginFlowAlias"
+                                ng-model="identityProvider.postBrokerLoginFlowAlias"
+                                ng-options="flow.alias as flow.alias for flow in postBrokerAuthFlows">
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'post-broker-login-flow.tooltip' | translate}}</kc-tooltip>
+            </div>
         </fieldset>
         <fieldset>
             <legend uncollapsed><span class="text">{{:: 'openid-connect-config' | translate}}</span> <kc-tooltip>{{:: 'openid-connect-config.tooltip' | translate}}</kc-tooltip></legend>
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 c8f6c37..2abfeb8 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 @@
                 </div>
                 <kc-tooltip>{{:: 'first-broker-login-flow.tooltip' | translate}}</kc-tooltip>
             </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="postBrokerLoginFlowAlias">{{:: 'post-broker-login-flow' | translate}}</label>
+                <div class="col-md-6">
+                    <div>
+                        <select class="form-control" id="postBrokerLoginFlowAlias"
+                                ng-model="identityProvider.postBrokerLoginFlowAlias"
+                                ng-options="flow.alias as flow.alias for flow in postBrokerAuthFlows">
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'post-broker-login-flow.tooltip' | translate}}</kc-tooltip>
+            </div>
         </fieldset>
         <fieldset>
             <legend uncollapsed><span class="text">{{:: 'saml-config' | translate}}</span> <kc-tooltip>{{:: 'identity-provider.saml-config.tooltip' | translate}}</kc-tooltip></legend>
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 5897f99..5090374 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 @@
                 </div>
                 <kc-tooltip>{{:: 'first-broker-login-flow.tooltip' | translate}}</kc-tooltip>
             </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="postBrokerLoginFlowAlias">{{:: 'post-broker-login-flow' | translate}}</label>
+                <div class="col-md-6">
+                    <div>
+                        <select class="form-control" id="postBrokerLoginFlowAlias"
+                                ng-model="identityProvider.postBrokerLoginFlowAlias"
+                                ng-options="flow.alias as flow.alias for flow in postBrokerAuthFlows">
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'post-broker-login-flow.tooltip' | translate}}</kc-tooltip>
+            </div>
         </fieldset>
 
         <div class="form-group">
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 f92f1d2..54a47bc 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<String, String> config = new HashMap<String, String>();
 
@@ -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/IdentityProviderModel.java b/model/api/src/main/java/org/keycloak/models/IdentityProviderModel.java
index 862f723..2f7e9af 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;
+
     /**
      * <p>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.</p>
@@ -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<String, String> getConfig() {
         return this.config;
     }
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 e60d915..1d34068 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 87c13a3..5328488 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/entities/IdentityProviderEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java
index 6bbc31c..2101de2 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<String, String> getConfig() {
         return this.config;
     }
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 3f58179..cfd7347 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/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 fc840ba..4229cd9 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<MongoRealmEntity> 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<MongoRealmEntity> 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<MongoRealmEntity> 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 b7c7663..60376cd 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 a4d0ea3..aa7ed77 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 5e732d8..616a3c4 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<String, String> 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 0000000..a7d9667
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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 968831c..2edbf6e 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/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index 89f8c39..eb3f816 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);
             }
 
+            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();
+                        .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);
             }
-        }  catch (Exception e) {
-            return redirectToErrorPage(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, e);
         }
     }
 
+
     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 36e4c6c..75dd87e 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, clientSessionn);
+        final String identityProviderAlias = 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"));
         }
-        BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, clientSession);
-        AuthenticationFlowModel firstBrokerLoginFlow = realm.getAuthenticationFlowById(brokerContext.getIdpConfig().getFirstBrokerLoginFlowId());
 
-        event.detail(Details.IDENTITY_PROVIDER, brokerContext.getIdpConfig().getAlias())
+        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/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java
index 51c6182..f41ee70 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/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java
index 36bf4c7..16ad0e3 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<AuthenticationExecutionModel> 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 7545a50..1c3e043 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 8e23f11..4dd34d7 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 0000000..dc8cc35
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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<FederatedIdentityModel> federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realmWithBroker);
+
+        List<String> 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 232b102..09782c9 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 816aa49..28cc044 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",