From adbf2b22ad4636a0e85f51a32559e4b51757ec31 Mon Sep 17 00:00:00 2001 From: mposolda Date: Thu, 29 Oct 2015 11:11:10 +0100 Subject: [PATCH] KEYCLOAK-1750 Improve first time login with social. Added 'first broker login' flow --- .../provider/AbstractIdentityProvider.java | 5 + .../provider/DefaultDataMarshaller.java | 40 +++ .../broker/provider/IdentityProvider.java | 6 + .../IdentityProviderDataMarshaller.java | 12 + broker/saml/pom.xml | 5 + .../broker/saml/SAMLDataMarshaller.java | 88 +++++ .../broker/saml/SAMLIdentityProvider.java | 5 + .../broker/saml/SAMLDataMarshallerTest.java | 64 ++++ .../org/keycloak/common/util/ObjectUtil.java | 2 +- .../META-INF/jpa-changelog-1.7.0.xml | 12 + .../META-INF/jpa-changelog-master.xml | 1 + .../jpa/updater/JpaUpdaterProvider.java | 2 +- .../idm/IdentityProviderRepresentation.java | 9 + .../main/java/org/keycloak/events/Errors.java | 1 + .../java/org/keycloak/events/EventType.java | 2 + ...KerberosUsernamePasswordAuthenticator.java | 11 + .../messages/admin-messages_en.properties | 2 + .../theme/base/admin/resources/js/app.js | 9 + .../admin/resources/js/controllers/realm.js | 10 +- .../realm-identity-provider-oidc.html | 13 + .../realm-identity-provider-saml.html | 13 + .../realm-identity-provider-social.html | 13 + .../base/login/login-idp-link-confirm.ftl | 21 ++ .../theme/base/login/login-idp-link-email.ftl | 15 + .../theme/base/login/login-update-profile.ftl | 2 +- .../main/resources/theme/base/login/login.ftl | 10 +- .../login/messages/messages_en.properties | 17 +- .../email/html/identity-provider-link.ftl | 5 + .../email/messages/messages_en.properties | 3 + .../email/text/identity-provider-link.ftl | 1 + .../org/keycloak/email/EmailProvider.java | 9 + .../freemarker/FreeMarkerEmailProvider.java | 39 ++- .../org/keycloak/login/LoginFormsPages.java | 4 +- .../keycloak/login/LoginFormsProvider.java | 13 + .../FreeMarkerLoginFormsProvider.java | 61 +++- .../login/freemarker/LoginFormsUtil.java | 58 ++++ .../keycloak/login/freemarker/Templates.java | 4 + .../model/IdentityProviderBean.java | 5 +- .../login/freemarker/model/ProfileBean.java | 10 +- .../login/freemarker/model/UrlBean.java | 4 + .../java/org/keycloak/models/Constants.java | 3 +- .../models/IdentityProviderModel.java | 11 + .../entities/IdentityProviderEntity.java | 9 + .../utils/DefaultAuthenticationFlows.java | 97 ++++++ .../models/utils/KeycloakModelUtils.java | 14 +- .../models/utils/ModelToRepresentation.java | 13 +- .../models/utils/RepresentationToModel.java | 37 +- .../org/keycloak/models/jpa/RealmAdapter.java | 3 + .../jpa/entities/IdentityProviderEntity.java | 12 + .../mongo/keycloak/adapters/RealmAdapter.java | 3 + saml/saml-core/pom.xml | 5 - .../saml/common/parsers/AbstractParser.java | 7 +- .../saml/v2/writers/SAMLAssertionWriter.java | 8 +- .../AuthenticationFlowError.java | 6 +- .../broker/AbstractIdpAuthenticator.java | 118 +++++++ .../broker/IdpConfirmLinkAuthenticator.java | 73 ++++ .../IdpConfirmLinkAuthenticatorFactory.java | 86 +++++ .../IdpDetectDuplicationsAuthenticator.java | 105 ++++++ ...etectDuplicationsAuthenticatorFactory.java | 85 +++++ .../IdpEmailVerificationAuthenticator.java | 135 ++++++++ ...EmailVerificationAuthenticatorFactory.java | 85 +++++ .../broker/IdpUpdateProfileAuthenticator.java | 118 +++++++ .../IdpUpdateProfileAuthenticatorFactory.java | 84 +++++ .../broker/IdpUsernamePasswordForm.java | 55 +++ .../IdpUsernamePasswordFormFactory.java | 35 ++ .../broker/util/ExistingUserInfo.java | 62 ++++ .../SerializedBrokeredIdentityContext.java | 327 ++++++++++++++++++ .../resetcred/ResetCredentialChooseUser.java | 14 + .../resetcred/ResetCredentialEmail.java | 7 +- .../requiredactions/UpdateProfile.java | 2 +- .../util/UpdateProfileContext.java | 38 ++ .../util/UserUpdateProfileContext.java | 81 +++++ .../main/java/org/keycloak/services/Urls.java | 14 + .../keycloak/services/messages/Messages.java | 12 +- .../services/resources/AccountService.java | 2 +- .../resources/AttributeFormDataProcessor.java | 8 +- .../resources/IdentityBrokerService.java | 240 +++++++------ .../resources/LoginActionsService.java | 101 +++++- .../admin/IdentityProviderResource.java | 4 +- .../admin/IdentityProvidersResource.java | 4 +- .../services/validation/Validation.java | 10 +- ...ycloak.authentication.AuthenticatorFactory | 5 + .../broker/AbstractIdentityProviderTest.java | 6 +- .../broker/ImportIdentityProviderTest.java | 18 +- .../broker-test/test-realm-with-broker.json | 1 + 85 files changed, 2573 insertions(+), 196 deletions(-) create mode 100644 broker/core/src/main/java/org/keycloak/broker/provider/DefaultDataMarshaller.java create mode 100644 broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderDataMarshaller.java create mode 100644 broker/saml/src/main/java/org/keycloak/broker/saml/SAMLDataMarshaller.java create mode 100644 broker/saml/src/test/java/org/keycloak/broker/saml/SAMLDataMarshallerTest.java create mode 100644 connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml create mode 100644 forms/common-themes/src/main/resources/theme/base/login/login-idp-link-confirm.ftl create mode 100644 forms/common-themes/src/main/resources/theme/base/login/login-idp-link-email.ftl create mode 100644 forms/common-themes/src/main/resources/theme/keycloak/email/html/identity-provider-link.ftl create mode 100644 forms/common-themes/src/main/resources/theme/keycloak/email/text/identity-provider-link.ftl create mode 100644 forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/LoginFormsUtil.java create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/broker/AbstractIdpAuthenticator.java create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpConfirmLinkAuthenticator.java create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpConfirmLinkAuthenticatorFactory.java create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpDetectDuplicationsAuthenticator.java create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpDetectDuplicationsAuthenticatorFactory.java create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticatorFactory.java create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUpdateProfileAuthenticator.java create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUpdateProfileAuthenticatorFactory.java create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordFormFactory.java create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/broker/util/ExistingUserInfo.java create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java create mode 100644 services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java create mode 100644 services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java index 6da630f557..a67741968e 100755 --- a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java +++ b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java @@ -92,4 +92,9 @@ public abstract class AbstractIdentityProvider public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) { } + + @Override + public IdentityProviderDataMarshaller getMarshaller() { + return new DefaultDataMarshaller(); + } } diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/DefaultDataMarshaller.java b/broker/core/src/main/java/org/keycloak/broker/provider/DefaultDataMarshaller.java new file mode 100644 index 0000000000..3f8fcf2f48 --- /dev/null +++ b/broker/core/src/main/java/org/keycloak/broker/provider/DefaultDataMarshaller.java @@ -0,0 +1,40 @@ +package org.keycloak.broker.provider; + +import java.io.IOException; + +import org.keycloak.common.util.Base64Url; +import org.keycloak.util.JsonSerialization; + +/** + * @author Marek Posolda + */ +public class DefaultDataMarshaller implements IdentityProviderDataMarshaller { + + @Override + public String serialize(Object value) { + if (value instanceof String) { + return (String) value; + } else { + try { + byte[] bytes = JsonSerialization.writeValueAsBytes(value); + return Base64Url.encode(bytes); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + } + + @Override + public T deserialize(String serialized, Class clazz) { + if (clazz.equals(String.class)) { + return clazz.cast(serialized); + } else { + byte[] bytes = Base64Url.decode(serialized); + try { + return JsonSerialization.readValue(bytes, clazz); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + } +} diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java index 1d775eec5c..42eb6fedf4 100755 --- a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java +++ b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java @@ -103,4 +103,10 @@ public interface IdentityProvider extends Provi */ Response export(UriInfo uriInfo, RealmModel realm, String format); + /** + * Implementation of marshaller to serialize/deserialize attached data to Strings, which can be saved in clientSession + * @return + */ + IdentityProviderDataMarshaller getMarshaller(); + } diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderDataMarshaller.java b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderDataMarshaller.java new file mode 100644 index 0000000000..7e57653613 --- /dev/null +++ b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderDataMarshaller.java @@ -0,0 +1,12 @@ +package org.keycloak.broker.provider; + +/** + * + * @author Marek Posolda + */ +public interface IdentityProviderDataMarshaller { + + String serialize(Object obj); + T deserialize(String serialized, Class clazz); + +} diff --git a/broker/saml/pom.xml b/broker/saml/pom.xml index 858a7ba21a..21a00994f0 100755 --- a/broker/saml/pom.xml +++ b/broker/saml/pom.xml @@ -45,6 +45,11 @@ jboss-logging provided + + junit + junit + test + diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLDataMarshaller.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLDataMarshaller.java new file mode 100644 index 0000000000..61f4d8af7a --- /dev/null +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLDataMarshaller.java @@ -0,0 +1,88 @@ +package org.keycloak.broker.saml; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +import javax.xml.stream.XMLEventReader; + +import org.keycloak.broker.provider.DefaultDataMarshaller; +import org.keycloak.dom.saml.v2.assertion.AssertionType; +import org.keycloak.dom.saml.v2.assertion.AuthnStatementType; +import org.keycloak.dom.saml.v2.protocol.ResponseType; +import org.keycloak.saml.common.exceptions.ParsingException; +import org.keycloak.saml.common.exceptions.ProcessingException; +import org.keycloak.saml.common.util.StaxUtil; +import org.keycloak.saml.processing.core.parsers.saml.SAMLParser; +import org.keycloak.saml.processing.core.parsers.util.SAMLParserUtil; +import org.keycloak.saml.processing.core.saml.v2.writers.SAMLAssertionWriter; +import org.keycloak.saml.processing.core.saml.v2.writers.SAMLResponseWriter; + +/** + * @author Marek Posolda + */ +public class SAMLDataMarshaller extends DefaultDataMarshaller { + + @Override + public String serialize(Object obj) { + + // Lame impl, but hopefully sufficient for now. See if something better is needed... + if (obj.getClass().getName().startsWith("org.keycloak.dom.saml")) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + try { + if (obj instanceof ResponseType) { + ResponseType responseType = (ResponseType) obj; + SAMLResponseWriter samlWriter = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(bos)); + samlWriter.write(responseType); + } else if (obj instanceof AssertionType) { + AssertionType assertion = (AssertionType) obj; + SAMLAssertionWriter samlWriter = new SAMLAssertionWriter(StaxUtil.getXMLStreamWriter(bos)); + samlWriter.write(assertion); + } else if (obj instanceof AuthnStatementType) { + AuthnStatementType authnStatement = (AuthnStatementType) obj; + SAMLAssertionWriter samlWriter = new SAMLAssertionWriter(StaxUtil.getXMLStreamWriter(bos)); + samlWriter.write(authnStatement, true); + } else { + throw new IllegalArgumentException("Don't know how to serialize object of type " + obj.getClass().getName()); + } + } catch (ProcessingException pe) { + throw new RuntimeException(pe); + } + + return new String(bos.toByteArray()); + } else { + return super.serialize(obj); + } + } + + @Override + public T deserialize(String serialized, Class clazz) { + if (clazz.getName().startsWith("org.keycloak.dom.saml")) { + String xmlString = serialized; + + try { + if (clazz.equals(ResponseType.class) || clazz.equals(AssertionType.class)) { + byte[] bytes = xmlString.getBytes(); + InputStream is = new ByteArrayInputStream(bytes); + Object respType = new SAMLParser().parse(is); + return clazz.cast(respType); + } else if (clazz.equals(AuthnStatementType.class)) { + byte[] bytes = xmlString.getBytes(); + InputStream is = new ByteArrayInputStream(bytes); + XMLEventReader xmlEventReader = new SAMLParser().createEventReader(is); + AuthnStatementType authnStatement = SAMLParserUtil.parseAuthnStatement(xmlEventReader); + return clazz.cast(authnStatement); + } else { + throw new IllegalArgumentException("Don't know how to deserialize object of type " + clazz.getName()); + } + } catch (ParsingException pe) { + throw new RuntimeException(pe); + } + + } else { + return super.deserialize(serialized, clazz); + } + } + +} diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java index 248c4de35b..0014470637 100755 --- a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java @@ -22,6 +22,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider; import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.broker.provider.IdentityProviderDataMarshaller; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AuthnStatementType; @@ -263,4 +264,8 @@ public class SAMLIdentityProvider extends AbstractIdentityProviderMarek Posolda + */ +public class SAMLDataMarshallerTest { + + private static final String TEST_RESPONSE = "http://localhost:8082/auth/realms/realm-with-saml-idp-basichttp://localhost:8082/auth/realms/realm-with-saml-idp-basictest-userhttp://localhost:8081/auth/realms/realm-with-brokerurn:oasis:names:tc:SAML:2.0:ac:classes:unspecified617-666-7777test-user@localhostmanager"; + + private static final String TEST_ASSERTION = "http://localhost:8082/auth/realms/realm-with-saml-idp-basictest-userhttp://localhost:8081/auth/realms/realm-with-brokerurn:oasis:names:tc:SAML:2.0:ac:classes:unspecified617-666-7777test-user@localhostmanager"; + + private static final String TEST_AUTHN_TYPE = "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified"; + + @Test + public void testParseResponse() throws Exception { + SAMLDataMarshaller serializer = new SAMLDataMarshaller(); + ResponseType responseType = serializer.deserialize(TEST_RESPONSE, ResponseType.class); + + // test ResponseType + Assert.assertEquals(responseType.getID(), "ID_4804cf50-cd96-4b92-823e-89adaa0c78ba"); + Assert.assertEquals(responseType.getDestination(), "http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint"); + Assert.assertEquals(responseType.getIssuer().getValue(), "http://localhost:8082/auth/realms/realm-with-saml-idp-basic"); + Assert.assertEquals(responseType.getAssertions().get(0).getID(), "ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9"); + + // back to String + String serialized = serializer.serialize(responseType); + Assert.assertEquals(TEST_RESPONSE, serialized); + } + + @Test + public void testParseAssertion() throws Exception { + SAMLDataMarshaller serializer = new SAMLDataMarshaller(); + AssertionType assertion = serializer.deserialize(TEST_ASSERTION, AssertionType.class); + + // test assertion + Assert.assertEquals(assertion.getID(), "ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9"); + Assert.assertEquals(((NameIDType) assertion.getSubject().getSubType().getBaseID()).getValue(), "test-user"); + + // back to String + String serialized = serializer.serialize(assertion); + Assert.assertEquals(TEST_ASSERTION, serialized); + } + + @Test + public void testParseAuthnType() throws Exception { + SAMLDataMarshaller serializer = new SAMLDataMarshaller(); + AuthnStatementType authnStatement = serializer.deserialize(TEST_AUTHN_TYPE, AuthnStatementType.class); + + // test authnStatement + Assert.assertEquals(authnStatement.getSessionIndex(), "fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5"); + + // back to String + String serialized = serializer.serialize(authnStatement); + Assert.assertEquals(TEST_AUTHN_TYPE, serialized); + } +} diff --git a/common/src/main/java/org/keycloak/common/util/ObjectUtil.java b/common/src/main/java/org/keycloak/common/util/ObjectUtil.java index cec9ea984b..1ade852c88 100644 --- a/common/src/main/java/org/keycloak/common/util/ObjectUtil.java +++ b/common/src/main/java/org/keycloak/common/util/ObjectUtil.java @@ -13,7 +13,7 @@ public class ObjectUtil { * @param str2 * @return true if both strings are null or equal */ - public static boolean isEqualOrNull(Object str1, Object str2) { + public static boolean isEqualOrBothNull(Object str1, Object str2) { if (str1 == null && str2 == null) { return true; } diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml new file mode 100644 index 0000000000..4f7d5fcd9f --- /dev/null +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml index 3010118039..2acc0bba74 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml @@ -10,4 +10,5 @@ + diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java index 401cf74b2d..a5429e4685 100755 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java @@ -12,7 +12,7 @@ public interface JpaUpdaterProvider extends Provider { public String FIRST_VERSION = "1.0.0.Final"; - public String LAST_VERSION = "1.6.1"; + public String LAST_VERSION = "1.7.0"; public String getCurrentVersionSql(String defaultSchema); diff --git a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java index 1e74002bc8..864d255dc0 100755 --- a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java @@ -52,6 +52,7 @@ public class IdentityProviderRepresentation { protected boolean storeToken; protected boolean addReadTokenRoleOnCreate; protected boolean authenticateByDefault; + protected String firstBrokerLoginFlowAlias; protected Map config = new HashMap(); public String getInternalId() { @@ -127,6 +128,14 @@ public class IdentityProviderRepresentation { this.authenticateByDefault = authenticateByDefault; } + public String getFirstBrokerLoginFlowAlias() { + return firstBrokerLoginFlowAlias; + } + + public void setFirstBrokerLoginFlowAlias(String firstBrokerLoginFlowAlias) { + this.firstBrokerLoginFlowAlias = firstBrokerLoginFlowAlias; + } + public boolean isStoreToken() { return this.storeToken; } diff --git a/events/api/src/main/java/org/keycloak/events/Errors.java b/events/api/src/main/java/org/keycloak/events/Errors.java index d7ca253778..34c5979c7c 100755 --- a/events/api/src/main/java/org/keycloak/events/Errors.java +++ b/events/api/src/main/java/org/keycloak/events/Errors.java @@ -53,4 +53,5 @@ public interface Errors { String EMAIL_SEND_FAILED = "email_send_failed"; String INVALID_EMAIL = "invalid_email"; String IDENTITY_PROVIDER_LOGIN_FAILURE = "identity_provider_login_failure"; + String IDENTITY_PROVIDER_ERROR = "identity_provider_error"; } diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java index 0a8b3aecee..5cffe7845c 100755 --- a/events/api/src/main/java/org/keycloak/events/EventType.java +++ b/events/api/src/main/java/org/keycloak/events/EventType.java @@ -60,6 +60,8 @@ public enum EventType { IDENTITY_PROVIDER_LOGIN(false), IDENTITY_PROVIDER_LOGIN_ERROR(false), + IDENTITY_PROVIDER_FIRST_LOGIN(true), + IDENTITY_PROVIDER_FIRST_LOGIN_ERROR(true), IDENTITY_PROVIDER_RESPONSE(false), IDENTITY_PROVIDER_RESPONSE_ERROR(false), IDENTITY_PROVIDER_RETRIEVE_TOKEN(false), diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java index ea3f953c5a..f38eeb5be1 100644 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java @@ -17,6 +17,7 @@ import javax.security.auth.login.LoginException; import org.jboss.logging.Logger; import org.keycloak.federation.kerberos.CommonKerberosConfig; +import org.keycloak.models.ModelException; /** * @author Marek Posolda @@ -54,6 +55,8 @@ public class KerberosUsernamePasswordAuthenticator { String message = le.getMessage(); logger.debug("Message from kerberos: " + message); + checkKerberosServerAvailable(le); + // Bit cumbersome, but seems to work with tested kerberos servers boolean exists = (!message.contains("Client not found")); return exists; @@ -74,11 +77,19 @@ public class KerberosUsernamePasswordAuthenticator { logoutSubject(); return true; } catch (LoginException le) { + checkKerberosServerAvailable(le); + logger.debug("Failed to authenticate user " + username, le); return false; } } + protected void checkKerberosServerAvailable(LoginException le) { + if (le.getMessage().contains("Port Unreachable")) { + throw new ModelException("Kerberos unreachable", le); + } + } + /** * Returns true if user was successfully authenticated against Kerberos 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 8c8f074196..ebf3a83e46 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 @@ -374,6 +374,7 @@ table-of-identity-providers=Table of identity providers add-provider.placeholder=Add provider... provider=Provider gui-order=GUI order +first-broker-login-flow=First Login Flow redirect-uri=Redirect URI redirect-uri.tooltip=The redirect uri to use when configuring the identity provider. alias=Alias @@ -393,6 +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. 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/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index f1d922b31a..96ee94d8aa 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -199,6 +199,9 @@ module.config([ '$routeProvider', function($routeProvider) { }, providerFactory : function(IdentityProviderFactoryLoader) { return {}; + }, + authFlows : function(AuthenticationFlowsLoader) { + return {}; } }, controller : 'RealmIdentityProviderCtrl' @@ -217,6 +220,9 @@ module.config([ '$routeProvider', function($routeProvider) { }, providerFactory : function(IdentityProviderFactoryLoader) { return new IdentityProviderFactoryLoader(); + }, + authFlows : function(AuthenticationFlowsLoader) { + return AuthenticationFlowsLoader(); } }, controller : 'RealmIdentityProviderCtrl' @@ -235,6 +241,9 @@ module.config([ '$routeProvider', function($routeProvider) { }, providerFactory : function(IdentityProviderFactoryLoader) { return IdentityProviderFactoryLoader(); + }, + authFlows : function(AuthenticationFlowsLoader) { + return AuthenticationFlowsLoader(); } }, controller : 'RealmIdentityProviderCtrl' diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index 7b321603be..37ec3abed2 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -594,7 +594,7 @@ module.controller('IdentityProviderTabCtrl', function(Dialog, $scope, Current, N }; }); -module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload, $http, $route, realm, instance, providerFactory, IdentityProvider, serverInfo, $location, Notifications, Dialog) { +module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload, $http, $route, realm, instance, providerFactory, IdentityProvider, serverInfo, authFlows, $location, Notifications, Dialog) { console.log('RealmIdentityProviderCtrl'); $scope.realm = angular.copy(realm); @@ -678,6 +678,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload $scope.identityProvider.enabled = true; $scope.identityProvider.updateProfileFirstLoginMode = "off"; $scope.identityProvider.authenticateByDefault = false; + $scope.identityProvider.firstBrokerLoginFlowAlias = 'first broker login'; $scope.newIdentityProvider = true; } @@ -696,6 +697,13 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload $scope.configuredProviders = angular.copy(realm.identityProviders); + $scope.authFlows = []; + for (var i=0 ; i {{:: 'gui-order.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'first-broker-login-flow.tooltip' | translate}} +
{{:: 'openid-connect-config' | translate}} {{:: 'openid-connect-config.tooltip' | translate}} diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html index bb9726f6e9..53cbf3a159 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html @@ -79,6 +79,19 @@ {{:: 'gui-order.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'first-broker-login-flow.tooltip' | translate}} +
{{:: 'saml-config' | translate}} {{:: 'identity-provider.saml-config.tooltip' | translate}} diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html index 2f71d197be..a4e3f1331b 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html @@ -97,6 +97,19 @@ {{:: 'gui-order.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'first-broker-login-flow.tooltip' | translate}} +
diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-confirm.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-confirm.ftl new file mode 100644 index 0000000000..28b12d0789 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-confirm.ftl @@ -0,0 +1,21 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=false; section> + <#if section = "title"> + ${msg("confirmLinkIdpTitle")} + <#elseif section = "header"> + ${msg("confirmLinkIdpTitle")} + <#elseif section = "form"> +
+

${message.summary}

+
+ +
+ +
+ + +
+ +
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-email.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-email.ftl new file mode 100644 index 0000000000..ab3e83e091 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-email.ftl @@ -0,0 +1,15 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "title"> + ${msg("emailLinkIdpTitle", idpAlias)} + <#elseif section = "header"> + ${msg("emailLinkIdpTitle", idpAlias)} + <#elseif section = "form"> +

+ ${msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.name)} +

+

+ ${msg("emailLinkIdp2")} ${msg("doClickHere")} ${msg("emailLinkIdp3")} +

+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl index 584bea322b..458884c849 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl @@ -6,7 +6,7 @@ ${msg("loginProfileTitle")} <#elseif section = "form">
- <#if realm.editUsernameAllowed> + <#if user.editUsernameAllowed>
diff --git a/forms/common-themes/src/main/resources/theme/base/login/login.ftl b/forms/common-themes/src/main/resources/theme/base/login/login.ftl index 925f99c207..5786807722 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/login.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login.ftl @@ -13,7 +13,11 @@
- + <#if usernameEditDisabled??> + + <#else> + +
@@ -29,7 +33,7 @@
- <#if realm.rememberMe> + <#if realm.rememberMe && !usernameEditDisabled??>