Merge pull request #1936 from mposolda/master

KEYCLOAK-2124 Post-Broker login flow support
This commit is contained in:
Marek Posolda 2015-12-09 17:35:00 +01:00
commit 06e44b185e
31 changed files with 656 additions and 60 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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;
}

View file

@ -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>

View file

@ -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),

View file

@ -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

View file

@ -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() {

View file

@ -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>

View file

@ -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>

View file

@ -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">

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

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

View file

@ -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;
}

View file

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

View file

@ -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);
}

View file

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

View file

@ -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);
}

View file

@ -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.";
}

View file

@ -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 {

View file

@ -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);
}

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

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

View file

@ -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;

View file

@ -42,6 +42,7 @@
"providerId" : "github",
"enabled": true,
"storeToken": "false",
"postBrokerLoginFlowAlias" : "browser",
"config": {
"authorizationUrl": "authorizationUrl",
"tokenUrl": "tokenUrl",