diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/auth-spi.xml b/docbook/auth-server-docs/reference/en/en-US/modules/auth-spi.xml
index 10cb89db71..12e2b1ac78 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/auth-spi.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/auth-spi.xml
@@ -866,6 +866,14 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
+
+ Modifying First Broker Login Flow
+
+ First Broker Login flow is used during first login with some identity provider. Term First Login means that there is not yet existing Keycloak account
+ linked with the particular authenticated identity provider account. More details about this flow are in the Identity provider chapter.
+
+
+
Authentication of clientsKeycloak actually supports pluggable authentication for OpenID Connect
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/identity-broker.xml b/docbook/auth-server-docs/reference/en/en-US/modules/identity-broker.xml
index a262b850fe..41c36f0c4f 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/identity-broker.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/identity-broker.xml
@@ -66,7 +66,7 @@
-
+ Overview
@@ -127,10 +127,11 @@
Now Keycloak is going to check if the response from the identity provider is valid. If valid, it will create an user
- or just skip that if the user already exists. If it is a new user, Keycloak will ask informations about the user to the identity provider
+ or just skip that if the user already exists. If it is a new user, Keycloak may ask informations about the user to the identity provider
(or just read that from a security token) and create the user locally. This is what we call identity federation.
- If the user already exists Keycloak will ask him to link the identity returned from the identity provider
- with his existing account. A process that we call account linking.
+ If the user already exists Keycloak may ask him to link the identity returned from the identity provider
+ with his existing account. A process that we call account linking. What exactly is done is configurable
+ and can be specified by setup of First Login Flow .
At the end of this step, Keycloak authenticates the user and issues its own token in order to access
the requested resource in the service provider.
@@ -210,7 +211,7 @@
Social providers allows you to enable social authentication to your realm.
Keycloak makes it easy to let users log in to your application using an existing account with a social network.
- Currently Facebook, Google and Twitter are supported with more planned for the future.
+ Currently Facebook, Google, Twitter, GitHub, LinkedIn and StackOverflow are supported with more planned for the future.
@@ -274,6 +275,15 @@
be used by any other means.
+
+
+ Authenticate By Default
+
+
+ If enabled, Keycloak will automatically redirect to this identity provider even before displaying login screen.
+ In other words, steps 3 and 4 from the base flow are skipped.
+
+ Store Tokens
@@ -293,20 +303,6 @@
to access any stored external tokens via the broker service.
-
-
- Update Profile on First Login
-
-
- Allows you to force users to update their profile right after the authentication finishes and
- before the account is actually created in Keycloak. When "On", users will be always presented with the
- update profile page asking for additional information in order to federate their identities.
- When "On missing info", users will be presented with the update profile page only if some
- mandatory information (email, first name, last name) is not provided by identity provider.
- If "Off", the account will be created with the minimal information obtained from the identity provider
- during the authentication process.
-
- Trust email
@@ -326,6 +322,16 @@
You can put number into this field, providers with lower numbers are shown first.
+
+
+ First Login Flow
+
+
+ Alias of authentication flow, which is triggered during 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.
+ More details in First Login section.
+
+
@@ -340,8 +346,8 @@
Forcing users to register to your realm when they want to access applications is hard.
So is trying to remember yet another username and password combination.
Social identity providers makes it easy for users to register on your realm and quickly sign in using a social network.
- Keycloak provides built-in support for the most common social networks out there, such as Google, Facebook, Twitter and
- even Github.
+ Keycloak provides built-in support for the most common social networks out there, such as Google, Facebook, Twitter,
+ Github, LinkedId and StackOverflow.
@@ -1211,7 +1217,13 @@ Authorization: Bearer {keycloak_access_token}]]>
Automatically Select and Identity Provider
- Applications can automatically select an identity provider in order to authenticate an user. In this case, the user will not be presented to the login page but automatically redirected to the identity provider.
+ Each Identity provider has option Authenticate By Default, which allows that Identity provider is automatically
+ selected during authentication. User won't even see Keycloak login page, but is automatically redirected to the identity provider.
+
+
+ Applications can also automatically select an identity provider in order to authenticate an user.
+ Selection per application is preferred over Authenticate By Default option if you need more control
+ on when exactly is Identity provider automatically selected.
Keycloak supports a specific HTTP query parameter that you can use as a hint to tell the server which identity provider should be used to authenticate the user.
@@ -1283,6 +1295,122 @@ keycloak.createLoginUrl({
+
+ First Login Flow
+
+ When Keycloak successfully authenticates user through identity provider (step 8 in Overview chapter),
+ there can be two situations:
+
+
+
+ There is already Keycloak user account linked with the authenticated identity provider account. In this case,
+ Keycloak will just authenticate as the existing user and redirect back to application (step 9 in Overview chapter).
+
+
+
+
+ There is not yet existing Keycloak user account linked with the identity provider account. This situation is more tricky.
+ Usually you just want to register new account into Keycloak database, but what if there is existing Keycloak account with same email like the identity provider account?
+ Automatically link identity provider account with existing Keycloak account is not very good option as there are possible security flaws related to that...
+
+
+
+
+
+ Because we had various requirements what to do in second case, we changed the behaviour to be flexible and configurable
+ through Authentication Flows SPI. In admin console in Identity provider settings, there is option
+ First Login Flow, which allows you to choose, which workflow will be used after "first login" with this identity provider account.
+ By default it points to first broker login flow, but you can configure and use your own flow and use different flows for different identity providers etc.
+
+
+ The flow itself is configured in admin console under Authentication tab. When you choose First Broker Login flow,
+ you will see what authenticators are used by default. You can either re-configure existing flow (For example disable some authenticators,
+ mark some of them as required, configure some authenticators etc). Or you can even create new authentication flow and/or
+ write your own Authenticator implementations and use it in your flow. See Authentication Flows SPI for more details on how to do it.
+
+
+ For First Broker Login case, it might be useful if your Authenticator is subclass of org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator
+ so you have access to all details about authenticated Identity provider account. But it's not a requirement.
+
+
+ Default First Login Flow
+
+ Let's describe the default behaviour provided by First Broker Login flow. There are those authenticators:
+
+
+ Review Profile
+
+
+ This authenticator might display the profile info page, where user can review his profile retrieved from identity provider.
+ The authenticator is configurable. You can set Update Profile On First Login option.
+ When On, users will be always presented with the profile page asking for additional information
+ in order to federate their identities. When missing, users will be presented with
+ the profile page only if some mandatory information (email, first name, last name) is not provided by identity provider.
+ If Off, the profile page won't be displayed, unless user clicks in later phase on Review profile info
+ link (page displayed in later phase by Confirm Link Existing Account authenticator)
+
+
+
+
+
+ Create User If Unique
+
+
+ This authenticator checks if there is already existing Keycloak account with same email or username like
+ the account from identity provider. If it's not, then authenticator just creates new Keyclok account and
+ link it with identity provider and whole flow is finished. Otherwise it goes to the next Handle Existing Account subflow.
+ If you always want to ensure that there is no duplicated account, you can mark this authenticator as REQUIRED .
+ In this case, the user will see the error page if there is existing Keycloak account and user needs
+ to link his identity provider account through Account management.
+
+
+ This authenticator also has config option Require Password Update After Registration .
+ When enabled, user is required to update password after account is created.
+
+
+
+
+
+ Confirm Link Existing Account
+
+
+ User will see the info page, that there is existing Keycloak account with same email. He can either
+ review his profile again and use different email or username (flow is restarted and goes back to Review Profile authenticator).
+ Or he can confirm that he wants to link identity provider account with his existing Keycloak account.
+ Disable this authenticator if you don't want users to see this confirmation page, but go straight
+ to linking identity provider account by email verification or re-authentication.
+
+
+
+
+
+ Verify Existing Account By Email
+
+
+ This authenticator is ALTERNATIVE by default, so it's used only if realm has SMTP setup configured.
+ It will send mail to user, where he can confirm that he wants to link identity provider with his Keycloak account.
+ Disable this if you don't want to confirm linking by email, but instead you always want users to reauthenticate with their password (and alternatively OTP).
+
+
+
+
+
+ Verify Existing Account By Re-authentication
+
+
+ This authenticator is used if email authenticator is disabled or non-available (SMTP not configured for realm). It
+ will display login screen where user needs to authenticate with his password to link his Keycloak account with Identity provider.
+ User can also re-authenticate with some different identity provider, which is already linked to his keycloak account.
+ You can also force users to use OTP, otherwise it's optional and used only if OTP is already set for user account.
+
+
+
+
+
+
+
+
+
Examples
diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
index 3d2eeea5fa..49801041cc 100755
--- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
@@ -133,6 +133,7 @@ federatedIdentityLinkNotActiveMessage=This identity is not active anymore.
federatedIdentityRemovingLastProviderMessage=You can''t remove last federated identity as you don''t have password.
identityProviderRedirectErrorMessage=Failed to redirect to identity provider.
identityProviderRemovedMessage=Identity provider removed successfully.
+identityProviderAlreadyLinkedMessage=Federated identity returned by {0} is already linked to another user.
accountDisabledMessage=Account is disabled, contact admin.
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index ebf3a83e46..bcbc98dc2d 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -394,7 +394,7 @@ update-profile-on-first-login.tooltip=Define conditions under which a user has t
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.
+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.
openid-connect-config=OpenID Connect Config
openid-connect-config.tooltip=OIDC SP and external IDP configuration.
authorization-url=Authorization URL
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
index 803ef2dcc0..68853082ca 100644
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -185,7 +185,6 @@ resetCredentialNotAllowedMessage=Reset Credential not allowed
permissionNotApprovedMessage=Permission not approved.
noRelayStateInResponseMessage=No relay state in response from identity provider.
-identityProviderAlreadyLinkedMessage=The identity returned by the identity provider is already linked to another user.
insufficientPermissionMessage=Insufficient permissions to link identities.
couldNotProceedWithAuthenticationRequestMessage=Could not proceed with authentication request to identity provider.
couldNotObtainTokenMessage=Could not obtain token from identity provider.
diff --git a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
index 98e960cc40..40a8999910 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
@@ -1,6 +1,8 @@
package org.keycloak.models.utils;
import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
import org.keycloak.models.AuthenticationExecutionModel;
@@ -36,7 +38,7 @@ public class DefaultAuthenticationFlows {
if (realm.getFlowByAlias(REGISTRATION_FLOW) == null) registrationFlow(realm);
if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
- if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm);
+ if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, false);
}
public static void migrateFlows(RealmModel realm) {
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
@@ -44,7 +46,7 @@ public class DefaultAuthenticationFlows {
if (realm.getFlowByAlias(REGISTRATION_FLOW) == null) registrationFlow(realm);
if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
- if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm);
+ if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, true);
}
public static void registrationFlow(RealmModel realm) {
@@ -322,7 +324,7 @@ public class DefaultAuthenticationFlows {
realm.addAuthenticatorExecution(execution);
}
- public static void firstBrokerLoginFlow(RealmModel realm) {
+ public static void firstBrokerLoginFlow(RealmModel realm, boolean migrate) {
AuthenticationFlowModel firstBrokerLogin = new AuthenticationFlowModel();
firstBrokerLogin.setAlias(FIRST_BROKER_LOGIN_FLOW);
firstBrokerLogin.setDescription("Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account");
@@ -423,10 +425,19 @@ public class DefaultAuthenticationFlows {
execution = new AuthenticationExecutionModel();
execution.setParentFlow(verifyByReauthenticationAccountFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL);
- // TODO: read the requirement from browser authenticator
-// if (migrate && hasCredentialType(realm, RequiredCredentialModel.TOTP.getType())) {
-// execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
-// }
+
+ if (migrate) {
+ // Try to read OTP requirement from browser flow
+ AuthenticationFlowModel browserFlow = realm.getBrowserFlow();
+ List browserExecutions = new LinkedList<>();
+ KeycloakModelUtils.deepFindAuthenticationExecutions(realm, browserFlow, browserExecutions);
+ for (AuthenticationExecutionModel browserExecution : browserExecutions) {
+ if (browserExecution.getAuthenticator().equals("auth-otp-form")) {
+ execution.setRequirement(browserExecution.getRequirement());
+ }
+ }
+ }
+
execution.setAuthenticator("auth-otp-form");
execution.setPriority(20);
execution.setAuthenticatorFlow(false);
diff --git a/model/api/src/main/java/org/keycloak/models/utils/FormMessage.java b/model/api/src/main/java/org/keycloak/models/utils/FormMessage.java
index b840de6dee..9f5c5deeb6 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/FormMessage.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/FormMessage.java
@@ -23,6 +23,9 @@ public class FormMessage {
private String message;
private Object[] parameters;
+ public FormMessage() {
+ }
+
/**
* Create message.
*
diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index c2fd73e7d9..ad5997a9a0 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -1,6 +1,8 @@
package org.keycloak.models.utils;
import org.bouncycastle.openssl.PEMWriter;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.GroupModel;
@@ -16,6 +18,7 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.common.util.CertificateUtils;
import org.keycloak.common.util.PemUtils;
@@ -31,6 +34,7 @@ import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.HashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -386,4 +390,24 @@ public final class KeycloakModelUtils {
realm.addDefaultRole(Constants.OFFLINE_ACCESS_ROLE);
}
}
+
+
+ /**
+ * Recursively find all AuthenticationExecutionModel from specified flow or all it's subflows
+ *
+ * @param realm
+ * @param flow
+ * @param result input should be empty list. At the end will be all executions added to this list
+ */
+ public static void deepFindAuthenticationExecutions(RealmModel realm, AuthenticationFlowModel flow, List result) {
+ List executions = realm.getAuthenticationExecutions(flow.getId());
+ for (AuthenticationExecutionModel execution : executions) {
+ if (execution.isAuthenticatorFlow()) {
+ AuthenticationFlowModel subFlow = realm.getAuthenticationFlowById(execution.getFlowId());
+ deepFindAuthenticationExecutions(realm, subFlow, result);
+ } else {
+ result.add(execution);
+ }
+ }
+ }
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 4a26cd83c3..7b8e3a6cca 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -1,5 +1,6 @@
package org.keycloak.models.jpa;
+import org.keycloak.Config;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.models.AuthenticationExecutionModel;
@@ -1195,8 +1196,13 @@ public class RealmAdapter implements RealmModel {
@Override
public ClientModel getMasterAdminClient() {
- ClientEntity client = realm.getMasterAdminClient();
- return client!=null ? new ClientAdapter(this, em, session, realm.getMasterAdminClient()) : null;
+ ClientEntity masterAdminClient = realm.getMasterAdminClient();
+ if (masterAdminClient == null) {
+ return null;
+ }
+
+ RealmAdapter masterRealm = new RealmAdapter(session, em, masterAdminClient.getRealm());
+ return new ClientAdapter(masterRealm, em, session, masterAdminClient);
}
@Override
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index 59bc5536f5..30266af27a 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -1223,7 +1223,13 @@ public class RealmAdapter extends AbstractMongoAdapter impleme
@Override
public ClientModel getMasterAdminClient() {
MongoClientEntity appData = getMongoStore().loadEntity(MongoClientEntity.class, realm.getMasterAdminClient(), invocationContext);
- return appData != null ? new ClientAdapter(session, this, appData, invocationContext) : null;
+ if (appData == null) {
+ return null;
+ }
+
+ MongoRealmEntity masterRealm = getMongoStore().loadEntity(MongoRealmEntity.class, appData.getRealmId(), invocationContext);
+ RealmModel masterRealmModel = new RealmAdapter(session, masterRealm, invocationContext);
+ return new ClientAdapter(session, masterRealmModel, appData, invocationContext);
}
@Override
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
index 87729d4a6b..83979c3dff 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
@@ -453,7 +453,7 @@ public class UserAdapter extends AbstractMongoAdapter implement
@Override
public Set getGroups() {
- if (user.getGroupIds() == null && user.getGroupIds().size() == 0) return Collections.EMPTY_SET;
+ if (user.getGroupIds() == null || user.getGroupIds().size() == 0) return Collections.EMPTY_SET;
Set groups = new HashSet<>();
for (String id : user.getGroupIds()) {
groups.add(realm.getGroupById(id));
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 2bebec3bfb..af24034223 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -61,6 +61,7 @@ import org.keycloak.services.messages.Messages;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.services.validation.Validation;
import org.keycloak.common.util.UriUtils;
+import org.keycloak.util.JsonSerialization;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
@@ -74,6 +75,8 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.core.Variant;
+
+import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.HashSet;
@@ -116,6 +119,9 @@ public class AccountService extends AbstractSecuredLocalService {
public static final String KEYCLOAK_STATE_CHECKER = "KEYCLOAK_STATE_CHECKER";
+ // Used when some other context (ie. IdentityBrokerService) wants to forward error to account management and display it here
+ public static final String ACCOUNT_MGMT_FORWARDED_ERROR_NOTE = "ACCOUNT_MGMT_FORWARDED_ERROR";
+
private final AppAuthManager authManager;
private EventBuilder event;
private AccountProvider account;
@@ -217,6 +223,17 @@ public class AccountService extends AbstractSecuredLocalService {
setReferrerOnPage();
+ String forwardedError = auth.getClientSession().getNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
+ if (forwardedError != null) {
+ try {
+ FormMessage errorMessage = JsonSerialization.readValue(forwardedError, FormMessage.class);
+ account.setError(errorMessage.getMessage(), errorMessage.getParameters());
+ auth.getClientSession().removeNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
+ }
+ }
+
return account.createResponse(page);
} else {
return login(path);
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index c8784bda2f..5912536c11 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -65,6 +65,7 @@ import org.keycloak.services.Urls;
import org.keycloak.services.validation.Validation;
import org.keycloak.social.SocialIdentityProvider;
import org.keycloak.common.util.ObjectUtil;
+import org.keycloak.util.JsonSerialization;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
@@ -74,6 +75,7 @@ import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
@@ -419,7 +421,6 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return finishBrokerAuthentication(context, federatedUser, clientSession, providerId);
}
} catch (Exception e) {
- // TODO?
return redirectToErrorPage(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, e);
}
}
@@ -465,7 +466,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
this.event.event(EventType.FEDERATED_IDENTITY_LINK);
if (federatedUser != null) {
- return redirectToErrorPage(Messages.IDENTITY_PROVIDER_ALREADY_LINKED, context.getIdpConfig().getAlias());
+ return redirectToAccountErrorPage(clientSession, Messages.IDENTITY_PROVIDER_ALREADY_LINKED, context.getIdpConfig().getAlias());
}
UserModel authenticatedUser = clientSession.getUserSession().getUser();
@@ -475,12 +476,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
if (!authenticatedUser.isEnabled()) {
- fireErrorEvent(Errors.USER_DISABLED);
- return redirectToErrorPage(Messages.ACCOUNT_DISABLED);
+ return redirectToAccountErrorPage(clientSession, Messages.ACCOUNT_DISABLED);
}
if (!authenticatedUser.hasRole(this.realmModel.getClientByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).getRole(MANAGE_ACCOUNT))) {
- fireErrorEvent(Errors.NOT_ALLOWED);
return redirectToErrorPage(Messages.INSUFFICIENT_PERMISSION);
}
@@ -581,6 +580,20 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return ErrorPage.error(this.session, message, parameters);
}
+ private Response redirectToAccountErrorPage(ClientSessionModel clientSession, String message, Object ... parameters) {
+ fireErrorEvent(message);
+
+ FormMessage errorMessage = new FormMessage(message, parameters);
+ try {
+ String serializedError = JsonSerialization.writeValueAsString(errorMessage);
+ clientSession.setNote(AccountService.ACCOUNT_MGMT_FORWARDED_ERROR_NOTE, serializedError);
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
+ }
+
+ return Response.status(302).location(UriBuilder.fromUri(clientSession.getRedirectUri()).build()).build();
+ }
+
private Response redirectToLoginPage(Throwable t, ClientSessionCode clientCode) {
String message = t.getMessage();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java
index 904caf6282..1d2e5b68d4 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java
@@ -389,6 +389,34 @@ public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdent
this.updateProfilePage.assertCurrent();
}
+
+ // KEYCLOAK-1822
+ @Test
+ public void testAccountManagementLinkedIdentityAlreadyExists() {
+ // Login as "test-user" through broker
+ IdentityProviderModel identityProvider = getIdentityProviderModel();
+ assertSuccessfulAuthentication(identityProvider, "test-user", "test-user@localhost", false);
+
+ // Login as pedroigor to account management
+ accountFederatedIdentityPage.realm("realm-with-broker");
+ accountFederatedIdentityPage.open();
+ assertTrue(driver.getTitle().equals("Log in to realm-with-broker"));
+ loginPage.login("pedroigor", "password");
+ assertTrue(accountFederatedIdentityPage.isCurrent());
+
+ // Try to link my "pedroigor" identity with "test-user" from brokered Keycloak.
+ accountFederatedIdentityPage.clickAddProvider(identityProvider.getAlias());
+
+ assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/"));
+ this.loginPage.login("test-user", "password");
+ doAfterProviderAuthentication();
+
+ // Error is displayed in account management because federated identity"test-user" already linked to local account "test-user"
+ assertTrue(accountFederatedIdentityPage.isCurrent());
+ assertEquals("Federated identity returned by " + getProviderId() + " is already linked to another user.", accountFederatedIdentityPage.getError());
+ }
+
+
@Test(expected = NoSuchElementException.class)
public void testIdentityProviderNotAllowed() {
this.driver.navigate().to("http://localhost:8081/test-app/");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
index 4e771dfefd..8c84581ae7 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
@@ -4,6 +4,7 @@ import org.junit.Assert;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
+import org.keycloak.Config;
import org.keycloak.models.ClientModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.ModelDuplicateException;
@@ -146,11 +147,11 @@ public class AdapterTest extends AbstractModelTest {
Assert.assertTrue(userProvider.validCredentials(realmModel, user, UserCredentialModel.password("geheim")));
List creds = user.getCredentialsDirectly();
Assert.assertEquals(creds.get(0).getHashIterations(), 1);
- realmModel.setPasswordPolicy( new PasswordPolicy("hashIterations(200)"));
+ realmModel.setPasswordPolicy(new PasswordPolicy("hashIterations(200)"));
Assert.assertTrue(userProvider.validCredentials(realmModel, user, UserCredentialModel.password("geheim")));
creds = user.getCredentialsDirectly();
Assert.assertEquals(creds.get(0).getHashIterations(), 200);
- realmModel.setPasswordPolicy( new PasswordPolicy("hashIterations(1)"));
+ realmModel.setPasswordPolicy(new PasswordPolicy("hashIterations(1)"));
}
@Test
@@ -797,6 +798,22 @@ public class AdapterTest extends AbstractModelTest {
}
+ // KEYCLOAK-2026
+ @Test
+ public void testMasterAdminClient() {
+ realmModel = realmManager.createRealm("foo-realm");
+ ClientModel masterAdminClient = realmModel.getMasterAdminClient();
+ Assert.assertEquals(Config.getAdminRealm(), masterAdminClient.getRealm().getId());
+
+ commit();
+
+ realmModel = realmManager.getRealmByName("foo-realm");
+ masterAdminClient = realmModel.getMasterAdminClient();
+ Assert.assertEquals(Config.getAdminRealm(), masterAdminClient.getRealm().getId());
+
+ realmManager.removeRealm(realmModel);
+ }
+
private KeyPair generateKeypair() throws NoSuchAlgorithmException {
return KeyPairGenerator.getInstance("RSA").generateKeyPair();
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountFederatedIdentityPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountFederatedIdentityPage.java
index 4c0279848f..54a5cbb4ba 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountFederatedIdentityPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountFederatedIdentityPage.java
@@ -5,12 +5,17 @@ import javax.ws.rs.core.UriBuilder;
import org.keycloak.services.Urls;
import org.keycloak.testsuite.Constants;
import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
/**
* @author Marek Posolda
*/
public class AccountFederatedIdentityPage extends AbstractAccountPage {
+ @FindBy(className = "alert-error")
+ private WebElement errorMessage;
+
public AccountFederatedIdentityPage() {};
private String realmName = "test";
@@ -39,4 +44,8 @@ public class AccountFederatedIdentityPage extends AbstractAccountPage {
public void clickRemoveProvider(String providerId) {
driver.findElement(By.id("remove-" + providerId)).click();
}
+
+ public String getError() {
+ return errorMessage.getText();
+ }
}