Skip to content

Commit

Permalink
KEYCLOAK-2124 Post-Broker login flow support
Browse files Browse the repository at this point in the history
  • Loading branch information
mposolda committed Dec 9, 2015
1 parent df26cae commit 081db0d
Show file tree
Hide file tree
Showing 30 changed files with 641 additions and 55 deletions.
@@ -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>
Expand Up @@ -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>
Expand Up @@ -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() {
Expand Down Expand Up @@ -139,6 +140,14 @@ public void setFirstBrokerLoginFlowAlias(String firstBrokerLoginFlowAlias) {
this.firstBrokerLoginFlowAlias = firstBrokerLoginFlowAlias;
}

public String getPostBrokerLoginFlowAlias() {
return postBrokerLoginFlowAlias;
}

public void setPostBrokerLoginFlowAlias(String postBrokerLoginFlowAlias) {
this.postBrokerLoginFlowAlias = postBrokerLoginFlowAlias;
}

public boolean isStoreToken() {
return this.storeToken;
}
Expand Down
2 changes: 2 additions & 0 deletions events/api/src/main/java/org/keycloak/events/EventType.java
Expand Up @@ -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),
Expand Down
Expand Up @@ -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
Expand All @@ -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
Expand Down
Expand Up @@ -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() {
Expand Down
Expand Up @@ -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>
Expand Down
Expand Up @@ -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>
Expand Down
Expand Up @@ -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">
Expand Down
Expand Up @@ -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>
Expand All @@ -78,6 +80,7 @@ public IdentityProviderModel(IdentityProviderModel model) {
this.authenticateByDefault = model.isAuthenticateByDefault();
this.addReadTokenRoleOnCreate = model.addReadTokenRoleOnCreate;
this.firstBrokerLoginFlowId = model.getFirstBrokerLoginFlowId();
this.postBrokerLoginFlowId = model.getPostBrokerLoginFlowId();
}

public String getInternalId() {
Expand Down Expand Up @@ -136,6 +139,14 @@ public void setFirstBrokerLoginFlowId(String firstBrokerLoginFlowId) {
this.firstBrokerLoginFlowId = firstBrokerLoginFlowId;
}

public String getPostBrokerLoginFlowId() {
return postBrokerLoginFlowId;
}

public void setPostBrokerLoginFlowId(String postBrokerLoginFlowId) {
this.postBrokerLoginFlowId = postBrokerLoginFlowId;
}

public Map<String, String> getConfig() {
return this.config;
}
Expand Down
Expand Up @@ -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>();

Expand Down Expand Up @@ -78,6 +79,14 @@ public void setFirstBrokerLoginFlowId(String firstBrokerLoginFlowId) {
this.firstBrokerLoginFlowId = firstBrokerLoginFlowId;
}

public String getPostBrokerLoginFlowId() {
return postBrokerLoginFlowId;
}

public void setPostBrokerLoginFlowId(String postBrokerLoginFlowId) {
this.postBrokerLoginFlowId = postBrokerLoginFlowId;
}

public boolean isStoreToken() {
return this.storeToken;
}
Expand Down
Expand Up @@ -512,6 +512,15 @@ public static IdentityProviderRepresentation toRepresentation(RealmModel realm,
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;
}

Expand Down
Expand Up @@ -1204,6 +1204,17 @@ public static IdentityProviderModel toModel(RealmModel realm, IdentityProviderRe
}
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;
}

Expand Down
Expand Up @@ -1277,6 +1277,7 @@ public List<IdentityProviderModel> getIdentityProviders() {
identityProviderModel.setTrustEmail(entity.isTrustEmail());
identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault());
identityProviderModel.setFirstBrokerLoginFlowId(entity.getFirstBrokerLoginFlowId());
identityProviderModel.setPostBrokerLoginFlowId(entity.getPostBrokerLoginFlowId());
identityProviderModel.setStoreToken(entity.isStoreToken());
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());

Expand Down Expand Up @@ -1310,6 +1311,7 @@ public void addIdentityProvider(IdentityProviderModel identityProvider) {
entity.setTrustEmail(identityProvider.isTrustEmail());
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
entity.setPostBrokerLoginFlowId(identityProvider.getPostBrokerLoginFlowId());
entity.setConfig(identityProvider.getConfig());

realm.addIdentityProvider(entity);
Expand Down Expand Up @@ -1337,6 +1339,7 @@ public void updateIdentityProvider(IdentityProviderModel identityProvider) {
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());
Expand Down
Expand Up @@ -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")
Expand Down Expand Up @@ -127,6 +130,14 @@ public void setFirstBrokerLoginFlowId(String firstBrokerLoginFlowId) {
this.firstBrokerLoginFlowId = firstBrokerLoginFlowId;
}

public String getPostBrokerLoginFlowId() {
return postBrokerLoginFlowId;
}

public void setPostBrokerLoginFlowId(String postBrokerLoginFlowId) {
this.postBrokerLoginFlowId = postBrokerLoginFlowId;
}

public Map<String, String> getConfig() {
return this.config;
}
Expand Down
Expand Up @@ -949,6 +949,7 @@ public List<IdentityProviderModel> getIdentityProviders() {
identityProviderModel.setTrustEmail(entity.isTrustEmail());
identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault());
identityProviderModel.setFirstBrokerLoginFlowId(entity.getFirstBrokerLoginFlowId());
identityProviderModel.setPostBrokerLoginFlowId(entity.getPostBrokerLoginFlowId());
identityProviderModel.setStoreToken(entity.isStoreToken());
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());

Expand Down Expand Up @@ -982,6 +983,7 @@ public void addIdentityProvider(IdentityProviderModel identityProvider) {
entity.setStoreToken(identityProvider.isStoreToken());
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
entity.setPostBrokerLoginFlowId(identityProvider.getPostBrokerLoginFlowId());
entity.setConfig(identityProvider.getConfig());

realm.getIdentityProviders().add(entity);
Expand All @@ -1008,6 +1010,7 @@ public void updateIdentityProvider(IdentityProviderModel identityProvider) {
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());
Expand Down
Expand Up @@ -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);
}
Expand All @@ -61,7 +61,7 @@ public void authenticate(AuthenticationFlowContext context) {
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);
}
Expand Down
Expand Up @@ -111,7 +111,7 @@ protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredI

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());

Expand Down
Expand Up @@ -41,7 +41,7 @@ protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap
}

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);
}
Expand Down
@@ -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.";
}
Expand Up @@ -301,17 +301,17 @@ public static SerializedBrokeredIdentityContext serialize(BrokeredIdentityContex
}

// 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 {
Expand Down

0 comments on commit 081db0d

Please sign in to comment.