Merge pull request #1936 from mposolda/master
KEYCLOAK-2124 Post-Broker login flow support
This commit is contained in:
commit
06e44b185e
31 changed files with 656 additions and 60 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>
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.";
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.jboss.resteasy.spi.HttpRequest;
|
|||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
||||
import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants;
|
||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
|
@ -33,7 +34,6 @@ import org.keycloak.broker.provider.IdentityProviderFactory;
|
|||
import org.keycloak.broker.provider.IdentityProviderMapper;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
|
@ -310,7 +310,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
clientSession.setTimestamp(Time.currentTime());
|
||||
|
||||
SerializedBrokeredIdentityContext ctx = SerializedBrokeredIdentityContext.serialize(context);
|
||||
ctx.saveToClientSession(clientSession);
|
||||
ctx.saveToClientSession(clientSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
|
||||
|
||||
URI redirect = LoginActionsService.firstBrokerLoginProcessor(uriInfo)
|
||||
.queryParam(OAuth2Constants.CODE, context.getCode())
|
||||
|
@ -319,21 +319,9 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
|
||||
} else {
|
||||
updateFederatedIdentity(context, federatedUser);
|
||||
clientSession.setAuthenticatedUser(federatedUser);
|
||||
|
||||
boolean firstBrokerLoginInProgress = (clientSession.getNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null);
|
||||
if (firstBrokerLoginInProgress) {
|
||||
LOGGER.debugf("Reauthenticated with broker '%s' when linking user '%s' with other broker", context.getIdpConfig().getAlias(), federatedUser.getUsername());
|
||||
|
||||
UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realmModel, clientSession);
|
||||
if (!linkingUser.getId().equals(federatedUser.getId())) {
|
||||
return redirectToErrorPage(Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE, federatedUser.getUsername(), linkingUser.getUsername());
|
||||
}
|
||||
|
||||
clientSession.setAuthenticatedUser(federatedUser);
|
||||
return afterFirstBrokerLogin(context.getCode());
|
||||
}
|
||||
|
||||
return finishBrokerAuthentication(context, federatedUser, clientSession, providerId);
|
||||
return finishOrRedirectToPostBrokerLogin(clientSession, context, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -345,13 +333,19 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
ClientSessionModel clientSession = clientCode.getClientSession();
|
||||
|
||||
try {
|
||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession);
|
||||
this.event.detail(Details.CODE_ID, clientSession.getId())
|
||||
.removeDetail("auth_method");
|
||||
|
||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
|
||||
if (serializedCtx == null) {
|
||||
throw new IdentityBrokerException("Not found serialized context in clientSession");
|
||||
}
|
||||
BrokeredIdentityContext context = serializedCtx.deserialize(session, clientSession);
|
||||
String providerId = context.getIdpConfig().getAlias();
|
||||
|
||||
event.detail(Details.IDENTITY_PROVIDER, providerId);
|
||||
event.detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername());
|
||||
|
||||
// firstBrokerLogin workflow finished. Removing note now
|
||||
clientSession.removeNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
|
||||
|
||||
|
@ -360,8 +354,15 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
throw new IdentityBrokerException("Couldn't found authenticated federatedUser in clientSession");
|
||||
}
|
||||
|
||||
event.user(federatedUser);
|
||||
event.detail(Details.USERNAME, federatedUser.getUsername());
|
||||
|
||||
if (context.getIdpConfig().isAddReadTokenRoleOnCreate()) {
|
||||
RoleModel readTokenRole = realmModel.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE);
|
||||
ClientModel brokerClient = realmModel.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID);
|
||||
if (brokerClient == null) {
|
||||
throw new IdentityBrokerException("Client 'broker' not available. Maybe realm has not migrated to support the broker token exchange service");
|
||||
}
|
||||
RoleModel readTokenRole = brokerClient.getRole(Constants.READ_TOKEN_ROLE);
|
||||
federatedUser.grantRole(readTokenRole);
|
||||
}
|
||||
|
||||
|
@ -370,12 +371,6 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
context.getUsername(), context.getToken());
|
||||
session.users().addFederatedIdentity(realmModel, federatedUser, federatedIdentityModel);
|
||||
|
||||
EventBuilder event = this.event.clone().user(federatedUser)
|
||||
.detail(Details.CODE_ID, clientSession.getId())
|
||||
.detail(Details.USERNAME, federatedUser.getUsername())
|
||||
.detail(Details.IDENTITY_PROVIDER, providerId)
|
||||
.detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername())
|
||||
.removeDetail("auth_method");
|
||||
|
||||
String isRegisteredNewUser = clientSession.getNote(AbstractIdpAuthenticator.BROKER_REGISTERED_NEW_USER);
|
||||
if (Boolean.parseBoolean(isRegisteredNewUser)) {
|
||||
|
@ -411,20 +406,108 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
updateFederatedIdentity(context, federatedUser);
|
||||
}
|
||||
|
||||
String isDifferentBrowser = clientSession.getNote(AbstractIdpAuthenticator.IS_DIFFERENT_BROWSER);
|
||||
if (Boolean.parseBoolean(isDifferentBrowser)) {
|
||||
session.sessions().removeClientSession(realmModel, clientSession);
|
||||
return session.getProvider(LoginFormsProvider.class)
|
||||
.setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, context.getIdpConfig().getAlias(), context.getUsername())
|
||||
.createInfoPage();
|
||||
} else {
|
||||
return finishBrokerAuthentication(context, federatedUser, clientSession, providerId);
|
||||
}
|
||||
return finishOrRedirectToPostBrokerLogin(clientSession, context, true);
|
||||
|
||||
} catch (Exception e) {
|
||||
return redirectToErrorPage(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Response finishOrRedirectToPostBrokerLogin(ClientSessionModel clientSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin) {
|
||||
String postBrokerLoginFlowId = context.getIdpConfig().getPostBrokerLoginFlowId();
|
||||
if (postBrokerLoginFlowId == null) {
|
||||
|
||||
LOGGER.debugf("Skip redirect to postBrokerLogin flow. PostBrokerLogin flow not set for identityProvider '%s'.", context.getIdpConfig().getAlias());
|
||||
return afterPostBrokerLoginFlowSuccess(clientSession, context, wasFirstBrokerLogin);
|
||||
} else {
|
||||
|
||||
LOGGER.debugf("Redirect to postBrokerLogin flow after authentication with identityProvider '%s'.", context.getIdpConfig().getAlias());
|
||||
|
||||
clientSession.setTimestamp(Time.currentTime());
|
||||
|
||||
SerializedBrokeredIdentityContext ctx = SerializedBrokeredIdentityContext.serialize(context);
|
||||
ctx.saveToClientSession(clientSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
|
||||
|
||||
clientSession.setNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN, String.valueOf(wasFirstBrokerLogin));
|
||||
|
||||
URI redirect = LoginActionsService.postBrokerLoginProcessor(uriInfo)
|
||||
.queryParam(OAuth2Constants.CODE, context.getCode())
|
||||
.build(realmModel.getName());
|
||||
return Response.status(302).location(redirect).build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Callback from LoginActionsService after postBrokerLogin flow is finished
|
||||
@GET
|
||||
@Path("/after-post-broker-login")
|
||||
public Response afterPostBrokerLoginFlow(@QueryParam("code") String code) {
|
||||
ClientSessionCode clientCode = parseClientSessionCode(code);
|
||||
ClientSessionModel clientSession = clientCode.getClientSession();
|
||||
|
||||
try {
|
||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
|
||||
if (serializedCtx == null) {
|
||||
throw new IdentityBrokerException("Not found serialized context in clientSession. Note " + PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT + " was null");
|
||||
}
|
||||
BrokeredIdentityContext context = serializedCtx.deserialize(session, clientSession);
|
||||
|
||||
String wasFirstBrokerLoginNote = clientSession.getNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN);
|
||||
boolean wasFirstBrokerLogin = Boolean.parseBoolean(wasFirstBrokerLoginNote);
|
||||
|
||||
// Ensure the post-broker-login flow was successfully finished
|
||||
String authStateNoteKey = PostBrokerLoginConstants.PBL_AUTH_STATE_PREFIX + context.getIdpConfig().getAlias();
|
||||
String authState = clientSession.getNote(authStateNoteKey);
|
||||
if (!Boolean.parseBoolean(authState)) {
|
||||
throw new IdentityBrokerException("Invalid request. Not found the flag that post-broker-login flow was finished");
|
||||
}
|
||||
|
||||
// remove notes
|
||||
clientSession.removeNote(PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
|
||||
clientSession.removeNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN);
|
||||
|
||||
return afterPostBrokerLoginFlowSuccess(clientSession, context, wasFirstBrokerLogin);
|
||||
} catch (IdentityBrokerException e) {
|
||||
return redirectToErrorPage(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Response afterPostBrokerLoginFlowSuccess(ClientSessionModel clientSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin) {
|
||||
String providerId = context.getIdpConfig().getAlias();
|
||||
UserModel federatedUser = clientSession.getAuthenticatedUser();
|
||||
|
||||
if (wasFirstBrokerLogin) {
|
||||
|
||||
String isDifferentBrowser = clientSession.getNote(AbstractIdpAuthenticator.IS_DIFFERENT_BROWSER);
|
||||
if (Boolean.parseBoolean(isDifferentBrowser)) {
|
||||
session.sessions().removeClientSession(realmModel, clientSession);
|
||||
return session.getProvider(LoginFormsProvider.class)
|
||||
.setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, context.getIdpConfig().getAlias(), context.getUsername())
|
||||
.createInfoPage();
|
||||
} else {
|
||||
return finishBrokerAuthentication(context, federatedUser, clientSession, providerId);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
boolean firstBrokerLoginInProgress = (clientSession.getNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null);
|
||||
if (firstBrokerLoginInProgress) {
|
||||
LOGGER.debugf("Reauthenticated with broker '%s' when linking user '%s' with other broker", context.getIdpConfig().getAlias(), federatedUser.getUsername());
|
||||
|
||||
UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realmModel, clientSession);
|
||||
if (!linkingUser.getId().equals(federatedUser.getId())) {
|
||||
return redirectToErrorPage(Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE, federatedUser.getUsername(), linkingUser.getUsername());
|
||||
}
|
||||
|
||||
return afterFirstBrokerLogin(context.getCode());
|
||||
} else {
|
||||
return finishBrokerAuthentication(context, federatedUser, clientSession, providerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Response finishBrokerAuthentication(BrokeredIdentityContext context, UserModel federatedUser, ClientSessionModel clientSession, String providerId) {
|
||||
UserSessionModel userSession = this.session.sessions()
|
||||
.createUserSession(this.realmModel, federatedUser, federatedUser.getUsername(), this.clientConnection.getRemoteAddr(), "broker", false, context.getBrokerSessionId(), context.getBrokerUserId());
|
||||
|
@ -443,6 +526,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
return AuthenticationProcessor.createRequiredActionRedirect(realmModel, clientSession, uriInfo);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Response cancelled(String code) {
|
||||
ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realmModel);
|
||||
|
|
|
@ -24,6 +24,7 @@ package org.keycloak.services.resources;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
||||
import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants;
|
||||
import org.keycloak.authentication.requiredactions.VerifyEmail;
|
||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
|
@ -98,6 +99,7 @@ public class LoginActionsService {
|
|||
public static final String RESET_CREDENTIALS_PATH = "reset-credentials";
|
||||
public static final String REQUIRED_ACTION = "required-action";
|
||||
public static final String FIRST_BROKER_LOGIN_PATH = "first-broker-login";
|
||||
public static final String POST_BROKER_LOGIN_PATH = "post-broker-login";
|
||||
|
||||
private RealmModel realm;
|
||||
|
||||
|
@ -144,6 +146,10 @@ public class LoginActionsService {
|
|||
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "firstBrokerLoginGet");
|
||||
}
|
||||
|
||||
public static UriBuilder postBrokerLoginProcessor(UriInfo uriInfo) {
|
||||
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "postBrokerLoginGet");
|
||||
}
|
||||
|
||||
public static UriBuilder loginActionsBaseUrl(UriBuilder baseUriBuilder) {
|
||||
return baseUriBuilder.path(RealmsResource.class).path(RealmsResource.class, "getLoginActionsService");
|
||||
}
|
||||
|
@ -407,7 +413,7 @@ public class LoginActionsService {
|
|||
|
||||
logger.debugf("Forget-password flow finished when authenticated user '%s' after first broker login.", linkingUser.getUsername());
|
||||
|
||||
return redirectToAfterFirstBrokerLoginEndpoint(clientSession);
|
||||
return redirectToAfterBrokerLoginEndpoint(clientSession, true);
|
||||
} else {
|
||||
return super.authenticationComplete();
|
||||
}
|
||||
|
@ -480,22 +486,39 @@ public class LoginActionsService {
|
|||
return processRegistration(execution, clientSession, null);
|
||||
}
|
||||
|
||||
|
||||
@Path(FIRST_BROKER_LOGIN_PATH)
|
||||
@GET
|
||||
public Response firstBrokerLoginGet(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution) {
|
||||
return firstBrokerLogin(code, execution);
|
||||
return brokerLoginFlow(code, execution, true);
|
||||
}
|
||||
|
||||
@Path(FIRST_BROKER_LOGIN_PATH)
|
||||
@POST
|
||||
public Response firstBrokerLoginPost(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution) {
|
||||
return firstBrokerLogin(code, execution);
|
||||
return brokerLoginFlow(code, execution, true);
|
||||
}
|
||||
|
||||
protected Response firstBrokerLogin(String code, String execution) {
|
||||
event.event(EventType.IDENTITY_PROVIDER_FIRST_LOGIN);
|
||||
@Path(POST_BROKER_LOGIN_PATH)
|
||||
@GET
|
||||
public Response postBrokerLoginGet(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution) {
|
||||
return brokerLoginFlow(code, execution, false);
|
||||
}
|
||||
|
||||
@Path(POST_BROKER_LOGIN_PATH)
|
||||
@POST
|
||||
public Response postBrokerLoginPost(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution) {
|
||||
return brokerLoginFlow(code, execution, false);
|
||||
}
|
||||
|
||||
|
||||
protected Response brokerLoginFlow(String code, String execution, final boolean firstBrokerLogin) {
|
||||
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
|
||||
event.event(eventType);
|
||||
|
||||
Checks checks = new Checks();
|
||||
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||
|
@ -503,16 +526,29 @@ public class LoginActionsService {
|
|||
}
|
||||
event.detail(Details.CODE_ID, code);
|
||||
ClientSessionCode clientSessionCode = checks.clientCode;
|
||||
ClientSessionModel clientSession = clientSessionCode.getClientSession();
|
||||
final ClientSessionModel clientSessionn = clientSessionCode.getClientSession();
|
||||
|
||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession);
|
||||
String noteKey = firstBrokerLogin ? AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE : PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT;
|
||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSessionn, noteKey);
|
||||
if (serializedCtx == null) {
|
||||
throw new WebApplicationException(ErrorPage.error(session, "Not found serialized context in clientSession"));
|
||||
logger.errorf("Not found serialized context in clientSession under note '%s'", noteKey);
|
||||
throw new WebApplicationException(ErrorPage.error(session, "Not found serialized context in clientSession."));
|
||||
}
|
||||
BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, clientSession);
|
||||
AuthenticationFlowModel firstBrokerLoginFlow = realm.getAuthenticationFlowById(brokerContext.getIdpConfig().getFirstBrokerLoginFlowId());
|
||||
BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, clientSessionn);
|
||||
final String identityProviderAlias = brokerContext.getIdpConfig().getAlias();
|
||||
|
||||
event.detail(Details.IDENTITY_PROVIDER, brokerContext.getIdpConfig().getAlias())
|
||||
String flowId = firstBrokerLogin ? brokerContext.getIdpConfig().getFirstBrokerLoginFlowId() : brokerContext.getIdpConfig().getPostBrokerLoginFlowId();
|
||||
if (flowId == null) {
|
||||
logger.errorf("Flow not configured for identity provider '%s'", identityProviderAlias);
|
||||
throw new WebApplicationException(ErrorPage.error(session, "Flow not configured for identity provider"));
|
||||
}
|
||||
AuthenticationFlowModel brokerLoginFlow = realm.getAuthenticationFlowById(flowId);
|
||||
if (brokerLoginFlow == null) {
|
||||
logger.errorf("Not found configured flow with ID '%s' for identity provider '%s'", flowId, identityProviderAlias);
|
||||
throw new WebApplicationException(ErrorPage.error(session, "Flow not found for identity provider"));
|
||||
}
|
||||
|
||||
event.detail(Details.IDENTITY_PROVIDER, identityProviderAlias)
|
||||
.detail(Details.IDENTITY_PROVIDER_USERNAME, brokerContext.getUsername());
|
||||
|
||||
|
||||
|
@ -520,19 +556,26 @@ public class LoginActionsService {
|
|||
|
||||
@Override
|
||||
protected Response authenticationComplete() {
|
||||
return redirectToAfterFirstBrokerLoginEndpoint(clientSession);
|
||||
if (!firstBrokerLogin) {
|
||||
String authStateNoteKey = PostBrokerLoginConstants.PBL_AUTH_STATE_PREFIX + identityProviderAlias;
|
||||
clientSessionn.setNote(authStateNoteKey, "true");
|
||||
}
|
||||
|
||||
return redirectToAfterBrokerLoginEndpoint(clientSession, firstBrokerLogin);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return processFlow(execution, clientSession, FIRST_BROKER_LOGIN_PATH, firstBrokerLoginFlow, null, processor);
|
||||
String flowPath = firstBrokerLogin ? FIRST_BROKER_LOGIN_PATH : POST_BROKER_LOGIN_PATH;
|
||||
return processFlow(execution, clientSessionn, flowPath, brokerLoginFlow, null, processor);
|
||||
}
|
||||
|
||||
private Response redirectToAfterFirstBrokerLoginEndpoint(ClientSessionModel clientSession) {
|
||||
private Response redirectToAfterBrokerLoginEndpoint(ClientSessionModel clientSession, boolean firstBrokerLogin) {
|
||||
ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession);
|
||||
clientSession.setTimestamp(Time.currentTime());
|
||||
|
||||
URI redirect = Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode());
|
||||
URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()) :
|
||||
Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()) ;
|
||||
logger.debugf("Redirecting to '%s' ", redirect);
|
||||
|
||||
return Response.status(302).location(redirect).build();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
"providerId" : "github",
|
||||
"enabled": true,
|
||||
"storeToken": "false",
|
||||
"postBrokerLoginFlowAlias" : "browser",
|
||||
"config": {
|
||||
"authorizationUrl": "authorizationUrl",
|
||||
"tokenUrl": "tokenUrl",
|
||||
|
|
Loading…
Reference in a new issue