Merge pull request #1827 from mposolda/broker-reauthentication

KEYCLOAK-1750 First broker login - migration, documentation. Other minor fixes
This commit is contained in:
Marek Posolda 2015-11-13 20:10:28 +01:00
commit ab436705c8
16 changed files with 312 additions and 42 deletions

View file

@ -866,6 +866,14 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
</para>
</section>
<section>
<title>Modifying First Broker Login Flow</title>
<para>
First Broker Login flow is used during first login with some identity provider. Term <literal>First Login</literal> 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 <link linkend="identity-broker-first-login">Identity provider chapter</link>.
</para>
</section>
<section id="client_authentication">
<title>Authentication of clients</title>
<para>Keycloak actually supports pluggable authentication for <ulink url="http://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect</ulink>

View file

@ -66,7 +66,7 @@
</itemizedlist>
<section>
<section id="identity-broker-overview">
<title>Overview</title>
<para>
@ -127,10 +127,11 @@
<listitem>
<para>
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 <emphasis>identity federation</emphasis>.
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 <emphasis>account linking</emphasis>.
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 <emphasis>account linking</emphasis>. What exactly is done is configurable
and can be specified by setup of <link linkend="identity-broker-first-login">First Login Flow</link> .
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.
</para>
@ -210,7 +211,7 @@
<para>
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.
</para>
</listitem>
</varlistentry>
@ -274,6 +275,15 @@
be used by any other means.
</entry>
</row>
<row>
<entry>
<literal>Authenticate By Default</literal>
</entry>
<entry>
If enabled, Keycloak will automatically redirect to this identity provider even before displaying login screen.
In other words, steps 3 and 4 from <link linkend="identity-broker-overview">the base flow</link> are skipped.
</entry>
</row>
<row>
<entry>
<literal>Store Tokens</literal>
@ -293,20 +303,6 @@
to access any stored external tokens via the broker service.
</entry>
</row>
<row>
<entry>
<literal>Update Profile on First Login</literal>
</entry>
<entry>
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
<emphasis>update profile page</emphasis> asking for additional information in order to federate their identities.
When "On missing info", users will be presented with the <emphasis>update profile page</emphasis> 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.
</entry>
</row>
<row>
<entry>
<literal>Trust email</literal>
@ -326,6 +322,16 @@
You can put number into this field, providers with lower numbers are shown first.
</entry>
</row>
<row>
<entry>
<literal>First Login Flow</literal>
</entry>
<entry>
Alias of authentication flow, which is triggered during first login with this identity provider. Term <literal>First Login</literal>
means that there is not yet existing Keycloak account linked with the authenticated identity provider account.
More details in <link linkend="identity-broker-first-login">First Login section</link>.
</entry>
</row>
</tbody>
</tgroup>
</table>
@ -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.
</para>
<section>
@ -1211,7 +1217,13 @@ Authorization: Bearer {keycloak_access_token}]]></programlisting>
<section>
<title>Automatically Select and Identity Provider</title>
<para>
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 <literal>Authenticate By Default</literal>, 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.
</para>
<para>
Applications can also automatically select an identity provider in order to authenticate an user.
Selection per application is preferred over <literal>Authenticate By Default</literal> option if you need more control
on when exactly is Identity provider automatically selected.
</para>
<para>
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({
</para>
</section>
<section id="identity-broker-first-login">
<title>First Login Flow</title>
<para>
When Keycloak successfully authenticates user through identity provider (step 8 in <link linkend="identity-broker-overview">Overview</link> chapter),
there can be two situations:
<orderedlist>
<listitem>
<para>
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 <link linkend="identity-broker-overview">Overview</link> chapter).
</para>
</listitem>
<listitem>
<para>
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...
</para>
</listitem>
</orderedlist>
</para>
<para>
Because we had various requirements what to do in second case, we changed the behaviour to be flexible and configurable
through <link linkend="auth_spi">Authentication Flows SPI</link>. In admin console in Identity provider settings, there is option
<literal>First Login Flow</literal>, which allows you to choose, which workflow will be used after "first login" with this identity provider account.
By default it points to <literal>first broker login</literal> flow, but you can configure and use your own flow and use different flows for different identity providers etc.
</para>
<para>
The flow itself is configured in admin console under <literal>Authentication</literal> tab. When you choose <literal>First Broker Login</literal> 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 <literal>required</literal>, 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 <link linkend="auth_spi">Authentication Flows SPI</link> for more details on how to do it.
</para>
<para>
For <literal>First Broker Login</literal> case, it might be useful if your Authenticator is subclass of <literal>org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator</literal>
so you have access to all details about authenticated Identity provider account. But it's not a requirement.
</para>
<section>
<title>Default First Login Flow</title>
<para>
Let's describe the default behaviour provided by <literal>First Broker Login</literal> flow. There are those authenticators:
<variablelist>
<varlistentry>
<term>Review Profile</term>
<listitem>
<para>
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 <literal>Update Profile On First Login</literal> option.
When <literal>On</literal>, users will be always presented with the profile page asking for additional information
in order to federate their identities. When <literal>missing</literal>, 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 <literal>Off</literal>, the profile page won't be displayed, unless user clicks in later phase on <literal>Review profile info</literal>
link (page displayed in later phase by <literal>Confirm Link Existing Account</literal> authenticator)
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>Create User If Unique</term>
<listitem>
<para>
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 <literal>Handle Existing Account</literal> subflow.
If you always want to ensure that there is no duplicated account, you can mark this authenticator as <literal>REQUIRED</literal> .
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.
</para>
<para>
This authenticator also has config option <literal>Require Password Update After Registration</literal> .
When enabled, user is required to update password after account is created.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>Confirm Link Existing Account</term>
<listitem>
<para>
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 <literal>Review Profile</literal> 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.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>Verify Existing Account By Email</term>
<listitem>
<para>
This authenticator is <literal>ALTERNATIVE</literal> 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).
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>Verify Existing Account By Re-authentication</term>
<listitem>
<para>
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.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</section>
</section>
<section>
<title>Examples</title>
<para>

View file

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

View file

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

View file

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

View file

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

View file

@ -23,6 +23,9 @@ public class FormMessage {
private String message;
private Object[] parameters;
public FormMessage() {
}
/**
* Create message.
*

View file

@ -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<AuthenticationExecutionModel> result) {
List<AuthenticationExecutionModel> 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);
}
}
}
}

View file

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

View file

@ -1223,7 +1223,13 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> 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

View file

@ -453,7 +453,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
@Override
public Set<GroupModel> 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<GroupModel> groups = new HashSet<>();
for (String id : user.getGroupIds()) {
groups.add(realm.getGroupById(id));

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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();
}
}