KEYCLOAK-1750 Improve first time login with social. Added 'first broker login' flow
This commit is contained in:
parent
2b29c3acf4
commit
adbf2b22ad
85 changed files with 2573 additions and 196 deletions
|
@ -92,4 +92,9 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel>
|
|||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderDataMarshaller getMarshaller() {
|
||||
return new DefaultDataMarshaller();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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> T deserialize(String serialized, Class<T> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -103,4 +103,10 @@ public interface IdentityProvider<C extends IdentityProviderModel> 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();
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package org.keycloak.broker.provider;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface IdentityProviderDataMarshaller {
|
||||
|
||||
String serialize(Object obj);
|
||||
<T> T deserialize(String serialized, Class<T> clazz);
|
||||
|
||||
}
|
|
@ -45,6 +45,11 @@
|
|||
<artifactId>jboss-logging</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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> T deserialize(String serialized, Class<T> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 AbstractIdentityProvider<SAMLIdentityP
|
|||
return SignatureAlgorithm.RSA_SHA256;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderDataMarshaller getMarshaller() {
|
||||
return new SAMLDataMarshaller();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package org.keycloak.broker.saml;
|
||||
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class SAMLDataMarshallerTest {
|
||||
|
||||
private static final String TEST_RESPONSE = "<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_4804cf50-cd96-4b92-823e-89adaa0c78ba\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.920Z\" Destination=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\" InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"></samlp:StatusCode></samlp:Status><saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.911Z\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><saml:Subject><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\">test-user</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\" NotOnOrAfter=\"2015-11-06T11:05:31.911Z\" Recipient=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\"></saml:SubjectConfirmationData></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"2015-11-06T11:00:31.911Z\" NotOnOrAfter=\"2015-11-06T11:01:31.911Z\"><saml:AudienceRestriction><saml:Audience>http://localhost:8081/auth/realms/realm-with-broker</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name=\"mobile\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">617-666-7777</saml:AttributeValue></saml:Attribute><saml:Attribute Name=\"urn:oid:1.2.840.113549.1.9.1\" FriendlyName=\"email\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">test-user@localhost</saml:AttributeValue></saml:Attribute></saml:AttributeStatement><saml:AttributeStatement><saml:Attribute Name=\"Role\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">manager</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>";
|
||||
|
||||
private static final String TEST_ASSERTION = "<saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.911Z\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><saml:Subject><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\">test-user</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\" NotOnOrAfter=\"2015-11-06T11:05:31.911Z\" Recipient=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\"></saml:SubjectConfirmationData></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"2015-11-06T11:00:31.911Z\" NotOnOrAfter=\"2015-11-06T11:01:31.911Z\"><saml:AudienceRestriction><saml:Audience>http://localhost:8081/auth/realms/realm-with-broker</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name=\"mobile\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">617-666-7777</saml:AttributeValue></saml:Attribute><saml:Attribute Name=\"urn:oid:1.2.840.113549.1.9.1\" FriendlyName=\"email\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">test-user@localhost</saml:AttributeValue></saml:Attribute></saml:AttributeStatement><saml:AttributeStatement><saml:Attribute Name=\"Role\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">manager</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion>";
|
||||
|
||||
private static final String TEST_AUTHN_TYPE = "<saml:AuthnStatement xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement>";
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
<changeSet author="mposolda@redhat.com" id="1.7.0">
|
||||
|
||||
<addColumn tableName="IDENTITY_PROVIDER">
|
||||
<column name="FIRST_BROKER_LOGIN_FLOW_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
|
@ -10,4 +10,5 @@
|
|||
<include file="META-INF/jpa-changelog-1.4.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-1.5.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-1.6.1.xml"/>
|
||||
<include file="META-INF/jpa-changelog-1.7.0.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ public class IdentityProviderRepresentation {
|
|||
protected boolean storeToken;
|
||||
protected boolean addReadTokenRoleOnCreate;
|
||||
protected boolean authenticateByDefault;
|
||||
protected String firstBrokerLoginFlowAlias;
|
||||
protected Map<String, String> config = new HashMap<String, String>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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<authFlows.length ; i++) {
|
||||
if (authFlows[i].providerId == 'basic-flow') {
|
||||
$scope.authFlows.push(authFlows[i]);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$watch(function() {
|
||||
return $location.path();
|
||||
}, function() {
|
||||
|
|
|
@ -79,6 +79,19 @@
|
|||
</div>
|
||||
<kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="firstBrokerLoginFlowAlias">{{:: 'first-broker-login-flow' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<select class="form-control" id="firstBrokerLoginFlowAlias"
|
||||
ng-model="identityProvider.firstBrokerLoginFlowAlias"
|
||||
ng-options="flow.alias as flow.alias for flow in authFlows"
|
||||
required>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'first-broker-login-flow.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend uncollapsed><span class="text">{{:: 'openid-connect-config' | translate}}</span> <kc-tooltip>{{:: 'openid-connect-config.tooltip' | translate}}</kc-tooltip></legend>
|
||||
|
|
|
@ -79,6 +79,19 @@
|
|||
</div>
|
||||
<kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="firstBrokerLoginFlowAlias">{{:: 'first-broker-login-flow' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<select class="form-control" id="firstBrokerLoginFlowAlias"
|
||||
ng-model="identityProvider.firstBrokerLoginFlowAlias"
|
||||
ng-options="flow.alias as flow.alias for flow in authFlows"
|
||||
required>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'first-broker-login-flow.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend uncollapsed><span class="text">{{:: 'saml-config' | translate}}</span> <kc-tooltip>{{:: 'identity-provider.saml-config.tooltip' | translate}}</kc-tooltip></legend>
|
||||
|
|
|
@ -97,6 +97,19 @@
|
|||
</div>
|
||||
<kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="firstBrokerLoginFlowAlias">{{:: 'first-broker-login-flow' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<select class="form-control" id="firstBrokerLoginFlowAlias"
|
||||
ng-model="identityProvider.firstBrokerLoginFlowAlias"
|
||||
ng-options="flow.alias as flow.alias for flow in authFlows"
|
||||
required>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'first-broker-login-flow.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-group">
|
||||
|
|
|
@ -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">
|
||||
<div id="kc-error-message">
|
||||
<p class="instruction">${message.summary}</p>
|
||||
</div>
|
||||
|
||||
<form id="kc-register-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
|
||||
|
||||
<div id="kc-form-buttons" class="${properties.kcFormGroupClass!}">
|
||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="updateProfile">${msg("confirmLinkIdpUpdateProfile")}</button>
|
||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="linkAccount">${msg("confirmLinkIdpContinue", idpAlias)}</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
|
@ -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">
|
||||
<p class="instruction">
|
||||
${msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.name)}
|
||||
</p>
|
||||
<p class="instruction">
|
||||
${msg("emailLinkIdp2")} <a href="${url.firstBrokerLoginUrl}">${msg("doClickHere")}</a> ${msg("emailLinkIdp3")}
|
||||
</p>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
|
@ -6,7 +6,7 @@
|
|||
${msg("loginProfileTitle")}
|
||||
<#elseif section = "form">
|
||||
<form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
|
||||
<#if realm.editUsernameAllowed>
|
||||
<#if user.editUsernameAllowed>
|
||||
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('username',properties.kcFormGroupErrorClass!)}">
|
||||
<div class="${properties.kcLabelWrapperClass!}">
|
||||
<label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>
|
||||
|
|
|
@ -13,7 +13,11 @@
|
|||
</div>
|
||||
|
||||
<div class="${properties.kcInputWrapperClass!}">
|
||||
<input id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')?html}" type="text" autofocus />
|
||||
<#if usernameEditDisabled??>
|
||||
<input id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')?html}" type="text" disabled />
|
||||
<#else>
|
||||
<input id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')?html}" type="text" autofocus />
|
||||
</#if>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -29,7 +33,7 @@
|
|||
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
|
||||
<#if realm.rememberMe>
|
||||
<#if realm.rememberMe && !usernameEditDisabled??>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<#if login.rememberMe??>
|
||||
|
@ -56,7 +60,7 @@
|
|||
</form>
|
||||
</#if>
|
||||
<#elseif section = "info" >
|
||||
<#if realm.password && realm.registrationAllowed>
|
||||
<#if realm.password && realm.registrationAllowed && !usernameEditDisabled??>
|
||||
<div id="kc-registration">
|
||||
<span>${msg("noAccount")} <a href="${url.registrationUrl}">${msg("doRegister")}</a></span>
|
||||
</div>
|
||||
|
|
|
@ -79,6 +79,11 @@ emailVerifyInstruction1=An email with instructions to verify your email address
|
|||
emailVerifyInstruction2=Haven''t received a verification code in your email?
|
||||
emailVerifyInstruction3=to re-send the email.
|
||||
|
||||
emailLinkIdpTitle=Link {0}
|
||||
emailLinkIdp1=An email with instructions to link {0} account {1} with your {2} account has been sent to you.
|
||||
emailLinkIdp2=Haven''t received a verification code in your email?
|
||||
emailLinkIdp3=to re-send the email.
|
||||
|
||||
backToLogin=« Back to Login
|
||||
|
||||
emailInstruction=Enter your username or email address and we will send you instructions on how to create a new password.
|
||||
|
@ -132,13 +137,19 @@ invalidTotpMessage=Invalid authenticator code.
|
|||
usernameExistsMessage=Username already exists.
|
||||
emailExistsMessage=Email already exists.
|
||||
|
||||
federatedIdentityEmailExistsMessage=User with email already exists. Please login to account management to link the account.
|
||||
federatedIdentityUsernameExistsMessage=User with username already exists. Please login to account management to link the account.
|
||||
federatedIdentityExistsMessage=User with {0} {1} already exists. Please login to account management to link the account.
|
||||
|
||||
confirmLinkIdpTitle=Account already exists
|
||||
federatedIdentityConfirmLinkMessage=User with {0} {1} already exists. How do you want to continue?
|
||||
federatedIdentityConfirmReauthenticateMessage=Authenticate as {0} to link your account with {1}
|
||||
confirmLinkIdpUpdateProfile=Update profile info
|
||||
confirmLinkIdpContinue=Link {0} with existing account
|
||||
|
||||
configureTotpMessage=You need to set up Mobile Authenticator to activate your account.
|
||||
updateProfileMessage=You need to update your user profile to activate your account.
|
||||
updatePasswordMessage=You need to change your password to activate your account.
|
||||
verifyEmailMessage=You need to verify your email address to activate your account.
|
||||
linkIdpMessage=You need to verify your email address to link your account with {0}.
|
||||
|
||||
emailSentMessage=You should receive an email shortly with further instructions.
|
||||
emailSendErrorMessage=Failed to send email, please try again later.
|
||||
|
@ -181,6 +192,7 @@ couldNotObtainTokenMessage=Could not obtain token from identity provider.
|
|||
unexpectedErrorRetrievingTokenMessage=Unexpected error when retrieving token from identity provider.
|
||||
unexpectedErrorHandlingResponseMessage=Unexpected error when handling response from identity provider.
|
||||
identityProviderAuthenticationFailedMessage=Authentication failed. Could not authenticate with identity provider.
|
||||
identityProviderDifferentUserMessage=Authenticated as {0}, but expected to be authenticated as {1}
|
||||
couldNotSendAuthenticationRequestMessage=Could not send authentication request to identity provider.
|
||||
unexpectedErrorHandlingRequestMessage=Unexpected error when handling authentication request to identity provider.
|
||||
invalidAccessCodeMessage=Invalid access code.
|
||||
|
@ -188,6 +200,7 @@ sessionNotActiveMessage=Session not active.
|
|||
invalidCodeMessage=An error occurred, please login again through your application.
|
||||
identityProviderUnexpectedErrorMessage=Unexpected error when authenticating with identity provider
|
||||
identityProviderNotFoundMessage=Could not find an identity provider with the identifier.
|
||||
identityProviderLinkSuccess=Your account was successfully linked with {0} account {1} .
|
||||
realmSupportsNoCredentialsMessage=Realm does not support any credential type.
|
||||
identityProviderNotUniqueMessage=Realm supports multiple identity providers. Could not determine which identity provider should be used to authenticate with.
|
||||
emailVerifiedMessage=Your email address has been verified.
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<html>
|
||||
<body>
|
||||
${msg("identityProviderLinkBodyHtml", identityProviderAlias, realmName, identityProviderContext.username, link, linkExpiration)}
|
||||
</body>
|
||||
</html>
|
|
@ -1,6 +1,9 @@
|
|||
emailVerificationSubject=Verify email
|
||||
emailVerificationBody=Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you didn''t create this account, just ignore this message.
|
||||
emailVerificationBodyHtml=<p>Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you didn''t create this account, just ignore this message.</p>
|
||||
identityProviderLinkSubject=Link {0}
|
||||
identityProviderLinkBody=Someone wants to link your "{1}" account with "{0}" account of user {2} . If this was you, click the link below to link accounts\n\n{3}\n\nThis link will expire within {4} minutes.\n\nIf you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.
|
||||
identityProviderLinkBodyHtml=<p>Someone wants to link your <b>{1}</b> account with <b>{0}</b> account of user {2} . If this was you, click the link below to link accounts</p><p><a href="{3}">{3}</a></p><p>This link will expire within {4} minutes.</p><p>If you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.</p>
|
||||
passwordResetSubject=Reset password
|
||||
passwordResetBody=Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.\n\n{0}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your credentials, just ignore this message and nothing will be changed.
|
||||
passwordResetBodyHtml=<p>Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your credentials, just ignore this message and nothing will be changed.</p>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
${msg("identityProviderLinkBody", identityProviderAlias, realmName, identityProviderContext.username, link, linkExpiration)}
|
|
@ -10,10 +10,14 @@ import org.keycloak.provider.Provider;
|
|||
*/
|
||||
public interface EmailProvider extends Provider {
|
||||
|
||||
String IDENTITY_PROVIDER_BROKER_CONTEXT = "identityProviderBrokerCtx";
|
||||
|
||||
public EmailProvider setRealm(RealmModel realm);
|
||||
|
||||
public EmailProvider setUser(UserModel user);
|
||||
|
||||
public EmailProvider setAttribute(String name, Object value);
|
||||
|
||||
public void sendEvent(Event event) throws EmailException;
|
||||
|
||||
/**
|
||||
|
@ -25,6 +29,11 @@ public interface EmailProvider extends Provider {
|
|||
*/
|
||||
public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException;
|
||||
|
||||
/**
|
||||
* Send to confirm that user wants to link his account with identity broker link
|
||||
*/
|
||||
void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException;
|
||||
|
||||
/**
|
||||
* Change password email requested by admin
|
||||
*
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package org.keycloak.email.freemarker;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
@ -17,6 +20,7 @@ import javax.mail.internet.MimeMessage;
|
|||
import javax.mail.internet.MimeMultipart;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.email.EmailException;
|
||||
import org.keycloak.email.EmailProvider;
|
||||
import org.keycloak.email.freemarker.beans.EventBean;
|
||||
|
@ -28,6 +32,7 @@ import org.keycloak.freemarker.FreeMarkerUtil;
|
|||
import org.keycloak.freemarker.Theme;
|
||||
import org.keycloak.freemarker.ThemeProvider;
|
||||
import org.keycloak.freemarker.beans.MessageFormatterMethod;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
@ -43,6 +48,7 @@ public class FreeMarkerEmailProvider implements EmailProvider {
|
|||
private FreeMarkerUtil freeMarker;
|
||||
private RealmModel realm;
|
||||
private UserModel user;
|
||||
private final Map<String, Object> attributes = new HashMap<String, Object>();
|
||||
|
||||
public FreeMarkerEmailProvider(KeycloakSession session, FreeMarkerUtil freeMarker) {
|
||||
this.session = session;
|
||||
|
@ -61,6 +67,12 @@ public class FreeMarkerEmailProvider implements EmailProvider {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmailProvider setAttribute(String name, Object value) {
|
||||
attributes.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendEvent(Event event) throws EmailException {
|
||||
Map<String, Object> attributes = new HashMap<String, Object>();
|
||||
|
@ -83,6 +95,27 @@ public class FreeMarkerEmailProvider implements EmailProvider {
|
|||
send("passwordResetSubject", "password-reset.ftl", attributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException {
|
||||
Map<String, Object> attributes = new HashMap<String, Object>();
|
||||
attributes.put("user", new ProfileBean(user));
|
||||
attributes.put("link", link);
|
||||
attributes.put("linkExpiration", expirationInMinutes);
|
||||
|
||||
String realmName = realm.getName().substring(0, 1).toUpperCase() + realm.getName().substring(1);
|
||||
attributes.put("realmName", realmName);
|
||||
|
||||
BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT);
|
||||
String idpAlias = brokerContext.getIdpConfig().getAlias();
|
||||
idpAlias = idpAlias.substring(0, 1).toUpperCase() + idpAlias.substring(1);
|
||||
|
||||
attributes.put("identityProviderContext", brokerContext);
|
||||
attributes.put("identityProviderAlias", idpAlias);
|
||||
|
||||
List<Object> subjectAttrs = Arrays.<Object>asList(idpAlias);
|
||||
send("identityProviderLinkSubject", subjectAttrs, "identity-provider-link.ftl", attributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendExecuteActions(String link, long expirationInMinutes) throws EmailException {
|
||||
Map<String, Object> attributes = new HashMap<String, Object>();
|
||||
|
@ -111,6 +144,10 @@ public class FreeMarkerEmailProvider implements EmailProvider {
|
|||
}
|
||||
|
||||
private void send(String subjectKey, String template, Map<String, Object> attributes) throws EmailException {
|
||||
send(subjectKey, Collections.emptyList(), template, attributes);
|
||||
}
|
||||
|
||||
private void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
|
||||
try {
|
||||
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
|
||||
Theme theme = themeProvider.getTheme(realm.getEmailTheme(), Theme.Type.EMAIL);
|
||||
|
@ -118,7 +155,7 @@ public class FreeMarkerEmailProvider implements EmailProvider {
|
|||
attributes.put("locale", locale);
|
||||
Properties rb = theme.getMessages(locale);
|
||||
attributes.put("msg", new MessageFormatterMethod(locale, rb));
|
||||
String subject = new MessageFormat(rb.getProperty(subjectKey,subjectKey),locale).format(new Object[0]);
|
||||
String subject = new MessageFormat(rb.getProperty(subjectKey,subjectKey),locale).format(subjectAttributes.toArray());
|
||||
String textTemplate = String.format("text/%s", template);
|
||||
String textBody;
|
||||
try {
|
||||
|
|
|
@ -5,6 +5,8 @@ package org.keycloak.login;
|
|||
*/
|
||||
public enum LoginFormsPages {
|
||||
|
||||
LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL, OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, INFO, ERROR, LOGIN_UPDATE_PROFILE, CODE;
|
||||
LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL,
|
||||
LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL,
|
||||
OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, INFO, ERROR, LOGIN_UPDATE_PROFILE, CODE;
|
||||
|
||||
}
|
||||
|
|
|
@ -23,6 +23,13 @@ import org.keycloak.provider.Provider;
|
|||
*/
|
||||
public interface LoginFormsProvider extends Provider {
|
||||
|
||||
String UPDATE_PROFILE_CONTEXT_ATTR = "updateProfileCtx";
|
||||
|
||||
String IDENTITY_PROVIDER_BROKER_CONTEXT = "identityProviderBrokerCtx";
|
||||
|
||||
String USERNAME_EDIT_DISABLED = "usernameEditDisabled";
|
||||
|
||||
|
||||
/**
|
||||
* Adds a script to the html header
|
||||
*
|
||||
|
@ -44,6 +51,12 @@ public interface LoginFormsProvider extends Provider {
|
|||
|
||||
public Response createInfoPage();
|
||||
|
||||
public Response createUpdateProfilePage();
|
||||
|
||||
public Response createIdpLinkConfirmLinkPage();
|
||||
|
||||
public Response createIdpLinkEmailPage();
|
||||
|
||||
public Response createErrorPage();
|
||||
|
||||
public Response createOAuthGrant(ClientSessionModel clientSessionModel);
|
||||
|
|
|
@ -19,6 +19,9 @@ package org.keycloak.login.freemarker;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
|
||||
import org.keycloak.authentication.requiredactions.util.UserUpdateProfileContext;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.email.EmailException;
|
||||
import org.keycloak.email.EmailProvider;
|
||||
import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
|
||||
|
@ -48,6 +51,7 @@ import org.keycloak.login.freemarker.model.UrlBean;
|
|||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -129,6 +133,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
page = LoginFormsPages.LOGIN_CONFIG_TOTP;
|
||||
break;
|
||||
case UPDATE_PROFILE:
|
||||
UpdateProfileContext userBasedContext = new UserUpdateProfileContext(realm, user);
|
||||
this.attributes.put(UPDATE_PROFILE_CONTEXT_ATTR, userBasedContext);
|
||||
|
||||
actionMessage = Messages.UPDATE_PROFILE;
|
||||
page = LoginFormsPages.LOGIN_UPDATE_PROFILE;
|
||||
break;
|
||||
|
@ -140,7 +147,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
try {
|
||||
UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
|
||||
builder.queryParam(OAuth2Constants.CODE, accessCode);
|
||||
builder.queryParam("key", clientSession.getNote(Constants.VERIFY_EMAIL_KEY));
|
||||
builder.queryParam(Constants.KEY, clientSession.getNote(Constants.VERIFY_EMAIL_KEY));
|
||||
|
||||
String link = builder.build(realm.getName()).toString();
|
||||
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
|
||||
|
@ -222,6 +229,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
}
|
||||
}
|
||||
attributes.put("message", wholeMessage);
|
||||
} else {
|
||||
attributes.put("message", null);
|
||||
}
|
||||
attributes.put("messagesPerField", messagesPerField);
|
||||
|
||||
|
@ -237,7 +246,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
|
||||
if (realm != null) {
|
||||
attributes.put("realm", new RealmBean(realm));
|
||||
attributes.put("social", new IdentityProviderBean(realm, baseUri, uriInfo));
|
||||
|
||||
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
|
||||
identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
|
||||
attributes.put("social", new IdentityProviderBean(realm, identityProviders, baseUri, uriInfo));
|
||||
|
||||
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
|
||||
|
||||
if (realm.isInternationalizationEnabled()) {
|
||||
|
@ -268,7 +281,17 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
attributes.put("totp", new TotpBean(realm, user, baseUri));
|
||||
break;
|
||||
case LOGIN_UPDATE_PROFILE:
|
||||
attributes.put("user", new ProfileBean(user, formData));
|
||||
UpdateProfileContext userCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
|
||||
attributes.put("user", new ProfileBean(userCtx, formData));
|
||||
break;
|
||||
case LOGIN_IDP_LINK_CONFIRM:
|
||||
case LOGIN_IDP_LINK_EMAIL:
|
||||
BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT);
|
||||
String idpAlias = brokerContext.getIdpConfig().getAlias();
|
||||
idpAlias = idpAlias.substring(0, 1).toUpperCase() + idpAlias.substring(1);
|
||||
|
||||
attributes.put("brokerContext", brokerContext);
|
||||
attributes.put("idpAlias", idpAlias);
|
||||
break;
|
||||
case REGISTER:
|
||||
attributes.put("register", new RegisterBean(formData));
|
||||
|
@ -371,7 +394,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
|
||||
if (realm != null) {
|
||||
attributes.put("realm", new RealmBean(realm));
|
||||
attributes.put("social", new IdentityProviderBean(realm, baseUri, uriInfo));
|
||||
|
||||
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
|
||||
identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
|
||||
attributes.put("social", new IdentityProviderBean(realm, identityProviders, baseUri, uriInfo));
|
||||
|
||||
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
|
||||
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
|
||||
|
||||
|
@ -423,6 +450,32 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
return createResponse(LoginFormsPages.INFO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response createUpdateProfilePage() {
|
||||
// Don't display initial message if we already have some errors
|
||||
if (messageType != MessageType.ERROR) {
|
||||
setMessage(MessageType.WARNING, Messages.UPDATE_PROFILE);
|
||||
}
|
||||
|
||||
return createResponse(LoginFormsPages.LOGIN_UPDATE_PROFILE);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Response createIdpLinkConfirmLinkPage() {
|
||||
return createResponse(LoginFormsPages.LOGIN_IDP_LINK_CONFIRM);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response createIdpLinkEmailPage() {
|
||||
BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT);
|
||||
String idpAlias = brokerContext.getIdpConfig().getAlias();
|
||||
idpAlias = idpAlias.substring(0, 1).toUpperCase() + idpAlias.substring(1);
|
||||
setMessage(MessageType.WARNING, Messages.LINK_IDP, idpAlias);
|
||||
|
||||
return createResponse(LoginFormsPages.LOGIN_IDP_LINK_EMAIL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response createErrorPage() {
|
||||
if (status == null) {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package org.keycloak.login.freemarker;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
/**
|
||||
* Various util methods, so the logic is not hardcoded in freemarker beans
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class LoginFormsUtil {
|
||||
|
||||
// Display just those identityProviders on login screen, which are already linked to "known" established user
|
||||
public static List<IdentityProviderModel> filterIdentityProviders(List<IdentityProviderModel> providers, KeycloakSession session, RealmModel realm,
|
||||
Map<String, Object> attributes, MultivaluedMap<String, String> formData) {
|
||||
|
||||
Boolean usernameEditDisabled = (Boolean) attributes.get(LoginFormsProvider.USERNAME_EDIT_DISABLED);
|
||||
if (usernameEditDisabled != null && usernameEditDisabled) {
|
||||
String username = formData.getFirst(UserModel.USERNAME);
|
||||
if (username == null) {
|
||||
throw new IllegalStateException("USERNAME_EDIT_DISABLED but username not known");
|
||||
}
|
||||
|
||||
UserModel user = session.users().getUserByUsername(username, realm);
|
||||
if (user == null || !user.isEnabled()) {
|
||||
throw new IllegalStateException("User " + username + " not found or disabled");
|
||||
}
|
||||
|
||||
Set<FederatedIdentityModel> fedLinks = session.users().getFederatedIdentities(user, realm);
|
||||
Set<String> federatedIdentities = new HashSet<>();
|
||||
for (FederatedIdentityModel fedLink : fedLinks) {
|
||||
federatedIdentities.add(fedLink.getIdentityProvider());
|
||||
}
|
||||
|
||||
List<IdentityProviderModel> result = new LinkedList<>();
|
||||
for (IdentityProviderModel idp : providers) {
|
||||
if (federatedIdentities.contains(idp.getAlias())) {
|
||||
result.add(idp);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return providers;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,6 +17,10 @@ public class Templates {
|
|||
return "login-config-totp.ftl";
|
||||
case LOGIN_VERIFY_EMAIL:
|
||||
return "login-verify-email.ftl";
|
||||
case LOGIN_IDP_LINK_CONFIRM:
|
||||
return "login-idp-link-confirm.ftl";
|
||||
case LOGIN_IDP_LINK_EMAIL:
|
||||
return "login-idp-link-email.ftl";
|
||||
case OAUTH_GRANT:
|
||||
return "login-oauth-grant.ftl";
|
||||
case LOGIN_RESET_PASSWORD:
|
||||
|
|
|
@ -44,9 +44,8 @@ public class IdentityProviderBean {
|
|||
private List<IdentityProvider> providers;
|
||||
private RealmModel realm;
|
||||
|
||||
public IdentityProviderBean(RealmModel realm, URI baseURI, UriInfo uriInfo) {
|
||||
public IdentityProviderBean(RealmModel realm, List<IdentityProviderModel> identityProviders, URI baseURI, UriInfo uriInfo) {
|
||||
this.realm = realm;
|
||||
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
|
||||
|
||||
if (!identityProviders.isEmpty()) {
|
||||
Set<IdentityProvider> orderedSet = new TreeSet<>(IdentityProviderComparator.INSTANCE);
|
||||
|
@ -57,7 +56,7 @@ public class IdentityProviderBean {
|
|||
}
|
||||
|
||||
if (!orderedSet.isEmpty()) {
|
||||
providers = new LinkedList<IdentityProvider>(orderedSet);
|
||||
providers = new LinkedList<>(orderedSet);
|
||||
displaySocial = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import java.util.Map;
|
|||
import javax.ws.rs.core.MultivaluedMap;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -38,12 +38,12 @@ public class ProfileBean {
|
|||
|
||||
private static final Logger logger = Logger.getLogger(ProfileBean.class);
|
||||
|
||||
private UserModel user;
|
||||
private UpdateProfileContext user;
|
||||
private MultivaluedMap<String, String> formData;
|
||||
|
||||
private final Map<String, String> attributes = new HashMap<>();
|
||||
|
||||
public ProfileBean(UserModel user, MultivaluedMap<String, String> formData) {
|
||||
public ProfileBean(UpdateProfileContext user, MultivaluedMap<String, String> formData) {
|
||||
this.user = user;
|
||||
this.formData = formData;
|
||||
|
||||
|
@ -70,6 +70,10 @@ public class ProfileBean {
|
|||
|
||||
}
|
||||
|
||||
public boolean isEditUsernameAllowed() {
|
||||
return user.isEditUsernameAllowed();
|
||||
}
|
||||
|
||||
public String getUsername() { return formData != null ? formData.getFirst("username") : user.getUsername(); }
|
||||
|
||||
public String getFirstName() {
|
||||
|
|
|
@ -90,6 +90,10 @@ public class UrlBean {
|
|||
return Urls.loginActionEmailVerification(baseURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getFirstBrokerLoginUrl() {
|
||||
return Urls.firstBrokerLoginProcessor(baseURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getOauthAction() {
|
||||
if (this.actionuri != null) {
|
||||
return this.actionuri.getPath();
|
||||
|
|
|
@ -23,5 +23,6 @@ public interface Constants {
|
|||
// 30 days
|
||||
int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
|
||||
|
||||
public static final String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
|
||||
String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
|
||||
String KEY = "key";
|
||||
}
|
||||
|
|
|
@ -64,6 +64,8 @@ public class IdentityProviderModel implements Serializable {
|
|||
*/
|
||||
private boolean authenticateByDefault;
|
||||
|
||||
private String firstBrokerLoginFlowId;
|
||||
|
||||
/**
|
||||
* <p>A map containing the configuration and properties for a specific identity provider instance and implementation. The items
|
||||
* in the map are understood by the identity provider implementation.</p>
|
||||
|
@ -84,6 +86,7 @@ public class IdentityProviderModel implements Serializable {
|
|||
this.storeToken = model.isStoreToken();
|
||||
this.authenticateByDefault = model.isAuthenticateByDefault();
|
||||
this.addReadTokenRoleOnCreate = model.addReadTokenRoleOnCreate;
|
||||
this.firstBrokerLoginFlowId = model.getFirstBrokerLoginFlowId();
|
||||
}
|
||||
|
||||
public String getInternalId() {
|
||||
|
@ -148,6 +151,14 @@ public class IdentityProviderModel implements Serializable {
|
|||
this.authenticateByDefault = authenticateByDefault;
|
||||
}
|
||||
|
||||
public String getFirstBrokerLoginFlowId() {
|
||||
return firstBrokerLoginFlowId;
|
||||
}
|
||||
|
||||
public void setFirstBrokerLoginFlowId(String firstBrokerLoginFlowId) {
|
||||
this.firstBrokerLoginFlowId = firstBrokerLoginFlowId;
|
||||
}
|
||||
|
||||
public Map<String, String> getConfig() {
|
||||
return this.config;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ public class IdentityProviderEntity {
|
|||
private boolean storeToken;
|
||||
protected boolean addReadTokenRoleOnCreate;
|
||||
private boolean authenticateByDefault;
|
||||
private String firstBrokerLoginFlowId;
|
||||
|
||||
private Map<String, String> config = new HashMap<String, String>();
|
||||
|
||||
|
@ -78,6 +79,14 @@ public class IdentityProviderEntity {
|
|||
this.authenticateByDefault = authenticateByDefault;
|
||||
}
|
||||
|
||||
public String getFirstBrokerLoginFlowId() {
|
||||
return firstBrokerLoginFlowId;
|
||||
}
|
||||
|
||||
public void setFirstBrokerLoginFlowId(String firstBrokerLoginFlowId) {
|
||||
this.firstBrokerLoginFlowId = firstBrokerLoginFlowId;
|
||||
}
|
||||
|
||||
public boolean isStoreToken() {
|
||||
return this.storeToken;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ public class DefaultAuthenticationFlows {
|
|||
public static final String LOGIN_FORMS_FLOW = "forms";
|
||||
|
||||
public static final String CLIENT_AUTHENTICATION_FLOW = "clients";
|
||||
public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login";
|
||||
|
||||
public static void addFlows(RealmModel realm) {
|
||||
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm);
|
||||
|
@ -26,6 +27,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);
|
||||
}
|
||||
public static void migrateFlows(RealmModel realm) {
|
||||
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
|
||||
|
@ -33,6 +35,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);
|
||||
}
|
||||
|
||||
public static void registrationFlow(RealmModel realm) {
|
||||
|
@ -309,4 +312,98 @@ public class DefaultAuthenticationFlows {
|
|||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
}
|
||||
|
||||
public static void firstBrokerLoginFlow(RealmModel realm) {
|
||||
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");
|
||||
firstBrokerLogin.setProviderId("basic-flow");
|
||||
firstBrokerLogin.setTopLevel(true);
|
||||
firstBrokerLogin.setBuiltIn(true);
|
||||
firstBrokerLogin = realm.addAuthenticationFlow(firstBrokerLogin);
|
||||
// realm.setClientAuthenticationFlow(clients);
|
||||
|
||||
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(firstBrokerLogin.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||
execution.setAuthenticator("idp-update-profile");
|
||||
execution.setPriority(10);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(firstBrokerLogin.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
|
||||
execution.setAuthenticator("idp-detect-duplications");
|
||||
execution.setPriority(20);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
AuthenticationFlowModel linkExistingAccountFlow = new AuthenticationFlowModel();
|
||||
linkExistingAccountFlow.setTopLevel(false);
|
||||
linkExistingAccountFlow.setBuiltIn(true);
|
||||
linkExistingAccountFlow.setAlias("Handle Existing Account");
|
||||
linkExistingAccountFlow.setDescription("Handle what to do if there is existing account with same email/username like authenticated identity provider");
|
||||
linkExistingAccountFlow.setProviderId("basic-flow");
|
||||
linkExistingAccountFlow = realm.addAuthenticationFlow(linkExistingAccountFlow);
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(firstBrokerLogin.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
|
||||
execution.setFlowId(linkExistingAccountFlow.getId());
|
||||
execution.setPriority(30);
|
||||
execution.setAuthenticatorFlow(true);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(linkExistingAccountFlow.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||
execution.setAuthenticator("idp-confirm-link");
|
||||
execution.setPriority(10);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(linkExistingAccountFlow.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
|
||||
execution.setAuthenticator("idp-email-verification");
|
||||
execution.setPriority(20);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
AuthenticationFlowModel verifyByReauthenticationAccountFlow = new AuthenticationFlowModel();
|
||||
verifyByReauthenticationAccountFlow.setTopLevel(false);
|
||||
verifyByReauthenticationAccountFlow.setBuiltIn(true);
|
||||
verifyByReauthenticationAccountFlow.setAlias("Verify Existing Account by Re-authentication");
|
||||
verifyByReauthenticationAccountFlow.setDescription("Reauthentication of existing account");
|
||||
verifyByReauthenticationAccountFlow.setProviderId("basic-flow");
|
||||
verifyByReauthenticationAccountFlow = realm.addAuthenticationFlow(verifyByReauthenticationAccountFlow);
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(linkExistingAccountFlow.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
|
||||
execution.setFlowId(verifyByReauthenticationAccountFlow.getId());
|
||||
execution.setPriority(30);
|
||||
execution.setAuthenticatorFlow(true);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
// password + otp
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(verifyByReauthenticationAccountFlow.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||
execution.setAuthenticator("idp-username-password-form");
|
||||
execution.setPriority(10);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
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);
|
||||
// }
|
||||
execution.setAuthenticator("auth-otp-form");
|
||||
execution.setPriority(20);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.keycloak.models.utils;
|
|||
import org.bouncycastle.openssl.PEMWriter;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.KeycloakSessionTask;
|
||||
|
@ -198,9 +199,9 @@ public final class KeycloakModelUtils {
|
|||
/**
|
||||
* Deep search if given role is descendant of composite role
|
||||
*
|
||||
* @param role role to check
|
||||
* @param role role to check
|
||||
* @param composite composite role
|
||||
* @param visited set of already visited roles (used for recursion)
|
||||
* @param visited set of already visited roles (used for recursion)
|
||||
* @return true if "role" is descendant of "composite"
|
||||
*/
|
||||
public static boolean searchFor(RoleModel role, RoleModel composite, Set<RoleModel> visited) {
|
||||
|
@ -218,14 +219,14 @@ public final class KeycloakModelUtils {
|
|||
/**
|
||||
* Try to find user by given username. If it fails, then fallback to find him by email
|
||||
*
|
||||
* @param realm realm
|
||||
* @param realm realm
|
||||
* @param username username or email of user
|
||||
* @return found user
|
||||
*/
|
||||
public static UserModel findUserByNameOrEmail(KeycloakSession session, RealmModel realm, String username) {
|
||||
UserModel user = session.users().getUserByUsername(username, realm);
|
||||
if (user == null && username.contains("@")) {
|
||||
user = session.users().getUserByEmail(username, realm);
|
||||
user = session.users().getUserByEmail(username, realm);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
@ -265,7 +266,6 @@ public final class KeycloakModelUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param roles
|
||||
* @param targetRole
|
||||
* @return true if targetRole is in roles (directly or indirectly via composite role)
|
||||
|
@ -284,8 +284,8 @@ public final class KeycloakModelUtils {
|
|||
/**
|
||||
* Ensure that displayName of myProvider (if not null) is unique and there is no other provider with same displayName in the list.
|
||||
*
|
||||
* @param displayName to check for duplications
|
||||
* @param myProvider provider, which is excluded from the list (if present)
|
||||
* @param displayName to check for duplications
|
||||
* @param myProvider provider, which is excluded from the list (if present)
|
||||
* @param federationProviders
|
||||
* @throws ModelDuplicateException if there is other provider with same displayName
|
||||
*/
|
||||
|
|
|
@ -202,7 +202,7 @@ public class ModelToRepresentation {
|
|||
}
|
||||
|
||||
for (IdentityProviderModel provider : realm.getIdentityProviders()) {
|
||||
rep.addIdentityProvider(toRepresentation(provider));
|
||||
rep.addIdentityProvider(toRepresentation(realm, provider));
|
||||
}
|
||||
|
||||
for (IdentityProviderMapperModel mapper : realm.getIdentityProviderMappers()) {
|
||||
|
@ -381,7 +381,7 @@ public class ModelToRepresentation {
|
|||
return rep;
|
||||
}
|
||||
|
||||
public static IdentityProviderRepresentation toRepresentation(IdentityProviderModel identityProviderModel) {
|
||||
public static IdentityProviderRepresentation toRepresentation(RealmModel realm, IdentityProviderModel identityProviderModel) {
|
||||
IdentityProviderRepresentation providerRep = new IdentityProviderRepresentation();
|
||||
|
||||
providerRep.setInternalId(identityProviderModel.getInternalId());
|
||||
|
@ -395,6 +395,15 @@ public class ModelToRepresentation {
|
|||
providerRep.setConfig(identityProviderModel.getConfig());
|
||||
providerRep.setAddReadTokenRoleOnCreate(identityProviderModel.isAddReadTokenRoleOnCreate());
|
||||
|
||||
String firstBrokerLoginFlowId = identityProviderModel.getFirstBrokerLoginFlowId();
|
||||
if (firstBrokerLoginFlowId != null) {
|
||||
AuthenticationFlowModel flow = realm.getAuthenticationFlowById(firstBrokerLoginFlowId);
|
||||
if (flow == null) {
|
||||
throw new ModelException("Couldn't find authentication flow with id " + firstBrokerLoginFlowId);
|
||||
}
|
||||
providerRep.setFirstBrokerLoginFlowAlias(flow.getAlias());
|
||||
}
|
||||
|
||||
return providerRep;
|
||||
}
|
||||
|
||||
|
|
|
@ -164,6 +164,16 @@ public class RepresentationToModel {
|
|||
if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep));
|
||||
else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
|
||||
|
||||
importAuthenticationFlows(newRealm, rep);
|
||||
if (rep.getRequiredActions() != null) {
|
||||
for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) {
|
||||
RequiredActionProviderModel model = toModel(action);
|
||||
newRealm.addRequiredActionProvider(model);
|
||||
}
|
||||
} else {
|
||||
DefaultRequiredActions.addActions(newRealm);
|
||||
}
|
||||
|
||||
importIdentityProviders(rep, newRealm);
|
||||
importIdentityProviderMappers(rep, newRealm);
|
||||
|
||||
|
@ -318,16 +328,6 @@ public class RepresentationToModel {
|
|||
if(rep.getDefaultLocale() != null){
|
||||
newRealm.setDefaultLocale(rep.getDefaultLocale());
|
||||
}
|
||||
|
||||
importAuthenticationFlows(newRealm, rep);
|
||||
if (rep.getRequiredActions() != null) {
|
||||
for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) {
|
||||
RequiredActionProviderModel model = toModel(action);
|
||||
newRealm.addRequiredActionProvider(model);
|
||||
}
|
||||
} else {
|
||||
DefaultRequiredActions.addActions(newRealm);
|
||||
}
|
||||
}
|
||||
|
||||
public static void importAuthenticationFlows(RealmModel newRealm, RealmRepresentation rep) {
|
||||
|
@ -1062,7 +1062,7 @@ public class RepresentationToModel {
|
|||
private static void importIdentityProviders(RealmRepresentation rep, RealmModel newRealm) {
|
||||
if (rep.getIdentityProviders() != null) {
|
||||
for (IdentityProviderRepresentation representation : rep.getIdentityProviders()) {
|
||||
newRealm.addIdentityProvider(toModel(representation));
|
||||
newRealm.addIdentityProvider(toModel(newRealm, representation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1073,7 +1073,7 @@ public class RepresentationToModel {
|
|||
}
|
||||
}
|
||||
}
|
||||
public static IdentityProviderModel toModel(IdentityProviderRepresentation representation) {
|
||||
public static IdentityProviderModel toModel(RealmModel realm, IdentityProviderRepresentation representation) {
|
||||
IdentityProviderModel identityProviderModel = new IdentityProviderModel();
|
||||
|
||||
identityProviderModel.setInternalId(representation.getInternalId());
|
||||
|
@ -1087,7 +1087,18 @@ public class RepresentationToModel {
|
|||
identityProviderModel.setAddReadTokenRoleOnCreate(representation.isAddReadTokenRoleOnCreate());
|
||||
identityProviderModel.setConfig(representation.getConfig());
|
||||
|
||||
return identityProviderModel;
|
||||
String flowAlias = representation.getFirstBrokerLoginFlowAlias();
|
||||
if (flowAlias == null) {
|
||||
flowAlias = DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW;
|
||||
}
|
||||
|
||||
AuthenticationFlowModel flowModel = realm.getFlowByAlias(flowAlias);
|
||||
if (flowModel == null) {
|
||||
throw new ModelException("No available authentication flow with alias: " + flowAlias);
|
||||
}
|
||||
identityProviderModel.setFirstBrokerLoginFlowId(flowModel.getId());
|
||||
|
||||
return identityProviderModel;
|
||||
}
|
||||
|
||||
public static ProtocolMapperModel toModel(ProtocolMapperRepresentation rep) {
|
||||
|
|
|
@ -1219,6 +1219,7 @@ public class RealmAdapter implements RealmModel {
|
|||
identityProviderModel.setUpdateProfileFirstLoginMode(entity.getUpdateProfileFirstLoginMode());
|
||||
identityProviderModel.setTrustEmail(entity.isTrustEmail());
|
||||
identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault());
|
||||
identityProviderModel.setFirstBrokerLoginFlowId(entity.getFirstBrokerLoginFlowId());
|
||||
identityProviderModel.setStoreToken(entity.isStoreToken());
|
||||
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());
|
||||
|
||||
|
@ -1252,6 +1253,7 @@ public class RealmAdapter implements RealmModel {
|
|||
entity.setUpdateProfileFirstLoginMode(identityProvider.getUpdateProfileFirstLoginMode());
|
||||
entity.setTrustEmail(identityProvider.isTrustEmail());
|
||||
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
||||
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
|
||||
entity.setConfig(identityProvider.getConfig());
|
||||
|
||||
realm.addIdentityProvider(entity);
|
||||
|
@ -1279,6 +1281,7 @@ public class RealmAdapter implements RealmModel {
|
|||
entity.setUpdateProfileFirstLoginMode(identityProvider.getUpdateProfileFirstLoginMode());
|
||||
entity.setTrustEmail(identityProvider.isTrustEmail());
|
||||
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
||||
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
|
||||
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
|
||||
entity.setStoreToken(identityProvider.isStoreToken());
|
||||
entity.setConfig(identityProvider.getConfig());
|
||||
|
|
|
@ -11,6 +11,7 @@ import javax.persistence.ManyToOne;
|
|||
import javax.persistence.MapKeyColumn;
|
||||
import javax.persistence.NamedQueries;
|
||||
import javax.persistence.NamedQuery;
|
||||
import javax.persistence.OneToOne;
|
||||
import javax.persistence.Table;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -56,6 +57,9 @@ public class IdentityProviderEntity {
|
|||
@Column(name="AUTHENTICATE_BY_DEFAULT")
|
||||
private boolean authenticateByDefault;
|
||||
|
||||
@Column(name="FIRST_BROKER_LOGIN_FLOW_ID")
|
||||
private String firstBrokerLoginFlowId;
|
||||
|
||||
@ElementCollection
|
||||
@MapKeyColumn(name="NAME")
|
||||
@Column(name="VALUE", columnDefinition = "TEXT")
|
||||
|
@ -126,6 +130,14 @@ public class IdentityProviderEntity {
|
|||
this.authenticateByDefault = authenticateByDefault;
|
||||
}
|
||||
|
||||
public String getFirstBrokerLoginFlowId() {
|
||||
return firstBrokerLoginFlowId;
|
||||
}
|
||||
|
||||
public void setFirstBrokerLoginFlowId(String firstBrokerLoginFlowId) {
|
||||
this.firstBrokerLoginFlowId = firstBrokerLoginFlowId;
|
||||
}
|
||||
|
||||
public Map<String, String> getConfig() {
|
||||
return this.config;
|
||||
}
|
||||
|
|
|
@ -826,6 +826,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
identityProviderModel.setUpdateProfileFirstLoginMode(entity.getUpdateProfileFirstLoginMode());
|
||||
identityProviderModel.setTrustEmail(entity.isTrustEmail());
|
||||
identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault());
|
||||
identityProviderModel.setFirstBrokerLoginFlowId(entity.getFirstBrokerLoginFlowId());
|
||||
identityProviderModel.setStoreToken(entity.isStoreToken());
|
||||
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());
|
||||
|
||||
|
@ -859,6 +860,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
|
||||
entity.setStoreToken(identityProvider.isStoreToken());
|
||||
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
||||
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
|
||||
entity.setConfig(identityProvider.getConfig());
|
||||
|
||||
realm.getIdentityProviders().add(entity);
|
||||
|
@ -885,6 +887,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
entity.setUpdateProfileFirstLoginMode(identityProvider.getUpdateProfileFirstLoginMode());
|
||||
entity.setTrustEmail(identityProvider.isTrustEmail());
|
||||
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
||||
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
|
||||
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
|
||||
entity.setStoreToken(identityProvider.isStoreToken());
|
||||
entity.setConfig(identityProvider.getConfig());
|
||||
|
|
|
@ -32,11 +32,6 @@
|
|||
<groupId>org.apache.santuario</groupId>
|
||||
<artifactId>xmlsec</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<resources>
|
||||
|
|
|
@ -76,6 +76,11 @@ public abstract class AbstractParser implements ParserNamespaceSupport {
|
|||
* @throws {@link IllegalArgumentException} when the configStream is null
|
||||
*/
|
||||
public Object parse(InputStream configStream) throws ParsingException {
|
||||
XMLEventReader xmlEventReader = createEventReader(configStream);
|
||||
return parse(xmlEventReader);
|
||||
}
|
||||
|
||||
public XMLEventReader createEventReader(InputStream configStream) throws ParsingException {
|
||||
if (configStream == null)
|
||||
throw logger.nullArgumentError("InputStream");
|
||||
|
||||
|
@ -105,7 +110,7 @@ public abstract class AbstractParser implements ParserNamespaceSupport {
|
|||
throw logger.parserException(e);
|
||||
}
|
||||
|
||||
return parse(xmlEventReader);
|
||||
return xmlEventReader;
|
||||
}
|
||||
|
||||
private ClassLoader getTCCL() {
|
||||
|
|
|
@ -136,7 +136,7 @@ public class SAMLAssertionWriter extends BaseWriter {
|
|||
if (statements != null) {
|
||||
for (StatementAbstractType statement : statements) {
|
||||
if (statement instanceof AuthnStatementType) {
|
||||
write((AuthnStatementType) statement);
|
||||
write((AuthnStatementType) statement, false);
|
||||
} else if (statement instanceof AttributeStatementType) {
|
||||
write((AttributeStatementType) statement);
|
||||
} else
|
||||
|
@ -188,8 +188,12 @@ public class SAMLAssertionWriter extends BaseWriter {
|
|||
*
|
||||
* @throws ProcessingException
|
||||
*/
|
||||
public void write(AuthnStatementType authnStatement) throws ProcessingException {
|
||||
public void write(AuthnStatementType authnStatement, boolean includeNamespace) throws ProcessingException {
|
||||
StaxUtil.writeStartElement(writer, ASSERTION_PREFIX, JBossSAMLConstants.AUTHN_STATEMENT.get(), ASSERTION_NSURI.get());
|
||||
if (includeNamespace) {
|
||||
StaxUtil.writeNameSpace(writer, ASSERTION_PREFIX, ASSERTION_NSURI.get());
|
||||
StaxUtil.writeDefaultNameSpace(writer, ASSERTION_NSURI.get());
|
||||
}
|
||||
|
||||
XMLGregorianCalendar authnInstant = authnStatement.getAuthnInstant();
|
||||
if (authnInstant != null) {
|
||||
|
|
|
@ -22,5 +22,9 @@ public enum AuthenticationFlowError {
|
|||
CLIENT_NOT_FOUND,
|
||||
CLIENT_DISABLED,
|
||||
CLIENT_CREDENTIALS_SETUP_REQUIRED,
|
||||
INVALID_CLIENT_CREDENTIALS
|
||||
INVALID_CLIENT_CREDENTIALS,
|
||||
|
||||
IDENTITY_PROVIDER_NOT_FOUND,
|
||||
IDENTITY_PROVIDER_DISABLED,
|
||||
IDENTITY_PROVIDER_ERROR
|
||||
}
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
package org.keycloak.authentication.authenticators.broker;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.AuthenticationFlowException;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.authenticators.broker.util.ExistingUserInfo;
|
||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public abstract class AbstractIdpAuthenticator implements Authenticator {
|
||||
|
||||
// The clientSession note encapsulating all the BrokeredIdentityContext info. When this note is in clientSession, we know that firstBrokerLogin flow is in progress
|
||||
public static final String BROKERED_CONTEXT_NOTE = "BROKERED_CONTEXT";
|
||||
|
||||
// The clientSession note with all the info about existing user
|
||||
public static final String EXISTING_USER_INFO = "EXISTING_USER_INFO";
|
||||
|
||||
// The clientSession note flag to indicate that email provided by identityProvider was changed on updateProfile page
|
||||
public static final String UPDATE_PROFILE_EMAIL_CHANGED = "UPDATE_PROFILE_EMAIL_CHANGED";
|
||||
|
||||
// The clientSession note flag to indicate if re-authentication after first broker login happened in different browser window. This can happen for example during email verification
|
||||
public static final String IS_DIFFERENT_BROWSER = "IS_DIFFERENT_BROWSER";
|
||||
|
||||
// The clientSession note flag to indicate that updateProfile page will be always displayed even if "updateProfileOnFirstLogin" is off
|
||||
public static final String ENFORCE_UPDATE_PROFILE = "ENFORCE_UPDATE_PROFILE";
|
||||
|
||||
// clientSession.note flag specifies if we imported new user to keycloak (true) or we just linked to an existing keycloak user (false)
|
||||
public static final String BROKER_REGISTERED_NEW_USER = "BROKER_REGISTERED_NEW_USER";
|
||||
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticationFlowContext context) {
|
||||
ClientSessionModel clientSession = context.getClientSession();
|
||||
|
||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession);
|
||||
if (serializedCtx == null) {
|
||||
throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR);
|
||||
}
|
||||
BrokeredIdentityContext brokerContext = serializedCtx.deserialize(context.getSession(), clientSession);
|
||||
|
||||
if (!brokerContext.getIdpConfig().isEnabled()) {
|
||||
sendFailureChallenge(context, Errors.IDENTITY_PROVIDER_ERROR, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, AuthenticationFlowError.IDENTITY_PROVIDER_ERROR);
|
||||
}
|
||||
|
||||
authenticateImpl(context, serializedCtx, brokerContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
ClientSessionModel clientSession = context.getClientSession();
|
||||
|
||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession);
|
||||
if (serializedCtx == null) {
|
||||
throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR);
|
||||
}
|
||||
BrokeredIdentityContext brokerContext = serializedCtx.deserialize(context.getSession(), clientSession);
|
||||
|
||||
if (!brokerContext.getIdpConfig().isEnabled()) {
|
||||
sendFailureChallenge(context, Errors.IDENTITY_PROVIDER_ERROR, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, AuthenticationFlowError.IDENTITY_PROVIDER_ERROR);
|
||||
}
|
||||
|
||||
actionImpl(context, serializedCtx, brokerContext);
|
||||
}
|
||||
|
||||
protected abstract void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext);
|
||||
protected abstract void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext);
|
||||
|
||||
protected void sendFailureChallenge(AuthenticationFlowContext context, String eventError, String errorMessage, AuthenticationFlowError flowError) {
|
||||
context.getEvent().user(context.getUser())
|
||||
.error(eventError);
|
||||
Response challengeResponse = context.form()
|
||||
.setError(errorMessage)
|
||||
.createErrorPage();
|
||||
context.failureChallenge(flowError, challengeResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
public static UserModel getExistingUser(KeycloakSession session, RealmModel realm, ClientSessionModel clientSession) {
|
||||
String existingUserId = clientSession.getNote(EXISTING_USER_INFO);
|
||||
if (existingUserId == null) {
|
||||
throw new AuthenticationFlowException("Unexpected state. There is no existing duplicated user identified in ClientSession",
|
||||
AuthenticationFlowError.INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
ExistingUserInfo duplication = ExistingUserInfo.deserialize(existingUserId);
|
||||
|
||||
UserModel existingUser = session.users().getUserById(duplication.getExistingUserId(), realm);
|
||||
if (existingUser == null) {
|
||||
throw new AuthenticationFlowException("User with ID '" + existingUserId + "' not found.", AuthenticationFlowError.INVALID_USER);
|
||||
}
|
||||
|
||||
if (!existingUser.isEnabled()) {
|
||||
throw new AuthenticationFlowException("User with ID '" + existingUserId + "', username '" + existingUser.getUsername() + "' disabled.", AuthenticationFlowError.USER_DISABLED);
|
||||
}
|
||||
|
||||
return existingUser;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package org.keycloak.authentication.authenticators.broker;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.AuthenticationFlowException;
|
||||
import org.keycloak.authentication.authenticators.broker.util.ExistingUserInfo;
|
||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class IdpConfirmLinkAuthenticator extends AbstractIdpAuthenticator {
|
||||
|
||||
protected static Logger logger = Logger.getLogger(IdpConfirmLinkAuthenticator.class);
|
||||
|
||||
@Override
|
||||
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
|
||||
ClientSessionModel clientSession = context.getClientSession();
|
||||
|
||||
String existingUserInfo = clientSession.getNote(EXISTING_USER_INFO);
|
||||
if (existingUserInfo == null) {
|
||||
logger.warnf("No duplication detected.");
|
||||
context.attempted();
|
||||
return;
|
||||
}
|
||||
|
||||
ExistingUserInfo duplicationInfo = ExistingUserInfo.deserialize(existingUserInfo);
|
||||
Response challenge = context.form()
|
||||
.setStatus(Response.Status.OK)
|
||||
.setAttribute(LoginFormsProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
|
||||
.setError(Messages.FEDERATED_IDENTITY_CONFIRM_LINK_MESSAGE, duplicationInfo.getDuplicateAttributeName(), duplicationInfo.getDuplicateAttributeValue())
|
||||
.createIdpLinkConfirmLinkPage();
|
||||
context.challenge(challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
|
||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||
|
||||
String action = formData.getFirst("submitAction");
|
||||
if (action != null && action.equals("updateProfile")) {
|
||||
context.getClientSession().setNote(ENFORCE_UPDATE_PROFILE, "true");
|
||||
context.getClientSession().removeNote(EXISTING_USER_INFO);
|
||||
context.resetFlow();
|
||||
} else if (action != null && action.equals("linkAccount")) {
|
||||
context.success();
|
||||
} else {
|
||||
throw new AuthenticationFlowException("Unknown action: " + action,
|
||||
AuthenticationFlowError.INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package org.keycloak.authentication.authenticators.broker;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class IdpConfirmLinkAuthenticatorFactory implements AuthenticatorFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "idp-confirm-link";
|
||||
static IdpConfirmLinkAuthenticator SINGLETON = new IdpConfirmLinkAuthenticator();
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
return SINGLETON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return "confirmLink";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return REQUIREMENT_CHOICES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Confirm link existing account";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Show the form where user confirms if he wants to link identity provider with existing account or rather edit user profile data retrieved from identity provider to avoid conflict";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserSetupAllowed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package org.keycloak.authentication.authenticators.broker;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.authenticators.broker.util.ExistingUserInfo;
|
||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class IdpDetectDuplicationsAuthenticator extends AbstractIdpAuthenticator {
|
||||
|
||||
protected static Logger logger = Logger.getLogger(IdpDetectDuplicationsAuthenticator.class);
|
||||
|
||||
|
||||
@Override
|
||||
protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
|
||||
|
||||
KeycloakSession session = context.getSession();
|
||||
RealmModel realm = context.getRealm();
|
||||
|
||||
if (context.getClientSession().getNote(EXISTING_USER_INFO) != null) {
|
||||
context.attempted();
|
||||
return;
|
||||
}
|
||||
|
||||
ExistingUserInfo duplication = checkExistingUser(context, serializedCtx, brokerContext);
|
||||
|
||||
if (duplication == null) {
|
||||
logger.debugf("No duplication detected. Creating account for user '%s' and linking with identity provider '%s' .",
|
||||
brokerContext.getModelUsername(), brokerContext.getIdpConfig().getAlias());
|
||||
|
||||
UserModel federatedUser = session.users().addUser(realm, brokerContext.getModelUsername());
|
||||
federatedUser.setEnabled(true);
|
||||
federatedUser.setEmail(brokerContext.getEmail());
|
||||
federatedUser.setFirstName(brokerContext.getFirstName());
|
||||
federatedUser.setLastName(brokerContext.getLastName());
|
||||
|
||||
for (Map.Entry<String, List<String>> attr : serializedCtx.getAttributes().entrySet()) {
|
||||
federatedUser.setAttribute(attr.getKey(), attr.getValue());
|
||||
}
|
||||
|
||||
// TODO: Event
|
||||
|
||||
context.setUser(federatedUser);
|
||||
context.getClientSession().setNote(BROKER_REGISTERED_NEW_USER, "true");
|
||||
context.success();
|
||||
} else {
|
||||
logger.debugf("Duplication detected. There is already existing user with %s '%s' .",
|
||||
duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue());
|
||||
|
||||
// Set duplicated user, so next authenticators can deal with it
|
||||
context.getClientSession().setNote(EXISTING_USER_INFO, duplication.serialize());
|
||||
|
||||
Response challengeResponse = context.form()
|
||||
.setError(Messages.FEDERATED_IDENTITY_EXISTS, duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue())
|
||||
.createErrorPage();
|
||||
context.challenge(challengeResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Could be overriden to detect duplication based on other criterias (firstName, lastName, ...)
|
||||
protected ExistingUserInfo checkExistingUser(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
|
||||
|
||||
if (brokerContext.getEmail() != null) {
|
||||
UserModel existingUser = context.getSession().users().getUserByEmail(brokerContext.getEmail(), context.getRealm());
|
||||
if (existingUser != null) {
|
||||
return new ExistingUserInfo(existingUser.getId(), UserModel.EMAIL, existingUser.getEmail());
|
||||
}
|
||||
}
|
||||
|
||||
UserModel existingUser = context.getSession().users().getUserByUsername(brokerContext.getModelUsername(), context.getRealm());
|
||||
if (existingUser != null) {
|
||||
return new ExistingUserInfo(existingUser.getId(), UserModel.USERNAME, existingUser.getUsername());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package org.keycloak.authentication.authenticators.broker;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class IdpDetectDuplicationsAuthenticatorFactory implements AuthenticatorFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "idp-detect-duplications";
|
||||
static IdpDetectDuplicationsAuthenticator SINGLETON = new IdpDetectDuplicationsAuthenticator();
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
return SINGLETON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return "createUserIfUnique";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return REQUIREMENT_CHOICES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Create User If Unique";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Detect if there is existing Keycloak account with same email like identity provider. If no, create new user";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserSetupAllowed() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
package org.keycloak.authentication.authenticators.broker;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.requiredactions.VerifyEmail;
|
||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.email.EmailException;
|
||||
import org.keycloak.email.EmailProvider;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator {
|
||||
|
||||
protected static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class);
|
||||
|
||||
@Override
|
||||
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
|
||||
KeycloakSession session = context.getSession();
|
||||
RealmModel realm = context.getRealm();
|
||||
ClientSessionModel clientSession = context.getClientSession();
|
||||
|
||||
if (realm.getSmtpConfig().size() == 0) {
|
||||
logger.warnf("Smtp is not configured for the realm. Ignoring email verification authenticator");
|
||||
context.attempted();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create action cookie to detect if email verification happened in same browser
|
||||
LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getClientSession().getId());
|
||||
|
||||
VerifyEmail.setupKey(clientSession);
|
||||
|
||||
UserModel existingUser = getExistingUser(session, realm, clientSession);
|
||||
|
||||
String link = UriBuilder.fromUri(context.getActionUrl())
|
||||
.queryParam(Constants.KEY, clientSession.getNote(Constants.VERIFY_EMAIL_KEY))
|
||||
.build().toString();
|
||||
long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
|
||||
try {
|
||||
|
||||
context.getSession().getProvider(EmailProvider.class)
|
||||
.setRealm(realm)
|
||||
.setUser(existingUser)
|
||||
.setAttribute(EmailProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
|
||||
.sendConfirmIdentityBrokerLink(link, expiration);
|
||||
// event.clone().event(EventType.SEND_RESET_PASSWORD)
|
||||
// .user(user)
|
||||
// .detail(Details.USERNAME, username)
|
||||
// .detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, context.getClientSession().getId()).success();
|
||||
} catch (EmailException e) {
|
||||
// event.clone().event(EventType.SEND_RESET_PASSWORD)
|
||||
// .detail(Details.USERNAME, username)
|
||||
// .user(user)
|
||||
// .error(Errors.EMAIL_SEND_FAILED);
|
||||
logger.error("Failed to send email to confirm identity broker linking", e);
|
||||
Response challenge = context.form()
|
||||
.setError(Messages.EMAIL_SENT_ERROR)
|
||||
.createErrorPage();
|
||||
context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
|
||||
return;
|
||||
}
|
||||
|
||||
Response challenge = context.form()
|
||||
.setStatus(Response.Status.OK)
|
||||
.setAttribute(LoginFormsProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
|
||||
.createIdpLinkEmailPage();
|
||||
context.forceChallenge(challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
|
||||
MultivaluedMap<String, String> queryParams = context.getSession().getContext().getUri().getQueryParameters();
|
||||
String key = queryParams.getFirst(Constants.KEY);
|
||||
ClientSessionModel clientSession = context.getClientSession();
|
||||
RealmModel realm = context.getRealm();
|
||||
KeycloakSession session = context.getSession();
|
||||
|
||||
if (key != null) {
|
||||
String keyFromSession = clientSession.getNote(Constants.VERIFY_EMAIL_KEY);
|
||||
clientSession.removeNote(Constants.VERIFY_EMAIL_KEY);
|
||||
if (key.equals(keyFromSession)) {
|
||||
UserModel existingUser = getExistingUser(session, realm, clientSession);
|
||||
|
||||
logger.debugf("User '%s' confirmed that wants to link with identity provider '%s' . Identity provider username is '%s' ", existingUser.getUsername(),
|
||||
brokerContext.getIdpConfig().getAlias(), brokerContext.getUsername());
|
||||
|
||||
String actionCookieValue = LoginActionsService.getActionCookie(session.getContext().getRequestHeaders(), realm, session.getContext().getUri(), context.getConnection());
|
||||
if (actionCookieValue == null || !actionCookieValue.equals(clientSession.getId())) {
|
||||
clientSession.setNote(IS_DIFFERENT_BROWSER, "true");
|
||||
}
|
||||
|
||||
context.setUser(existingUser);
|
||||
context.success();
|
||||
} else {
|
||||
logger.error("Key parameter don't match with the expected value from client session");
|
||||
Response challengeResponse = context.form()
|
||||
.setError(Messages.INVALID_ACCESS_CODE)
|
||||
.createErrorPage();
|
||||
context.failureChallenge(AuthenticationFlowError.IDENTITY_PROVIDER_ERROR, challengeResponse);
|
||||
}
|
||||
} else {
|
||||
Response challengeResponse = context.form()
|
||||
.setError(Messages.MISSING_PARAMETER, Constants.KEY)
|
||||
.createErrorPage();
|
||||
context.failureChallenge(AuthenticationFlowError.IDENTITY_PROVIDER_ERROR, challengeResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package org.keycloak.authentication.authenticators.broker;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class IdpEmailVerificationAuthenticatorFactory implements AuthenticatorFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "idp-email-verification";
|
||||
static IdpEmailVerificationAuthenticator SINGLETON = new IdpEmailVerificationAuthenticator();
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
return SINGLETON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return "emailVerification";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return REQUIREMENT_CHOICES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Verify existing account by Email";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Email verification of existing Keycloak user, that wants to link his user account with identity provider";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserSetupAllowed() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package org.keycloak.authentication.authenticators.broker;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.common.util.ObjectUtil;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.services.resources.AttributeFormDataProcessor;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class IdpUpdateProfileAuthenticator extends AbstractIdpAuthenticator {
|
||||
|
||||
protected static Logger logger = Logger.getLogger(IdpUpdateProfileAuthenticator.class);
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext userCtx, BrokeredIdentityContext brokerContext) {
|
||||
IdentityProviderModel idpConfig = brokerContext.getIdpConfig();
|
||||
|
||||
if (requiresUpdateProfilePage(context, userCtx, brokerContext)) {
|
||||
|
||||
logger.debugf("Identity provider '%s' requires update profile action for broker user '%s'.", idpConfig.getAlias(), userCtx.getUsername());
|
||||
|
||||
// No formData for first render. The profile is rendered from userCtx
|
||||
Response challengeResponse = context.form()
|
||||
.setAttribute(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR, userCtx)
|
||||
.setFormData(null)
|
||||
.createUpdateProfilePage();
|
||||
context.challenge(challengeResponse);
|
||||
} else {
|
||||
// Not required to update profile. Marked success
|
||||
context.success();
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean requiresUpdateProfilePage(AuthenticationFlowContext context, SerializedBrokeredIdentityContext userCtx, BrokeredIdentityContext brokerContext) {
|
||||
String enforceUpdateProfile = context.getClientSession().getNote(ENFORCE_UPDATE_PROFILE);
|
||||
if (Boolean.parseBoolean(enforceUpdateProfile)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
IdentityProviderModel idpConfig = brokerContext.getIdpConfig();
|
||||
RealmModel realm = context.getRealm();
|
||||
return IdentityProviderRepresentation.UPFLM_ON.equals(idpConfig.getUpdateProfileFirstLoginMode())
|
||||
|| (IdentityProviderRepresentation.UPFLM_MISSING.equals(idpConfig.getUpdateProfileFirstLoginMode()) && !Validation.validateUserMandatoryFields(realm, userCtx));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext userCtx, BrokeredIdentityContext brokerContext) {
|
||||
EventBuilder event = context.getEvent();
|
||||
event.event(EventType.UPDATE_PROFILE);
|
||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||
|
||||
RealmModel realm = context.getRealm();
|
||||
|
||||
List<FormMessage> errors = Validation.validateUpdateProfileForm(true, formData);
|
||||
if (errors != null && !errors.isEmpty()) {
|
||||
Response challenge = context.form()
|
||||
.setErrors(errors)
|
||||
.setAttribute(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR, userCtx)
|
||||
.setFormData(formData)
|
||||
.createUpdateProfilePage();
|
||||
context.challenge(challenge);
|
||||
return;
|
||||
}
|
||||
|
||||
userCtx.setUsername(formData.getFirst(UserModel.USERNAME));
|
||||
userCtx.setFirstName(formData.getFirst(UserModel.FIRST_NAME));
|
||||
userCtx.setLastName(formData.getFirst(UserModel.LAST_NAME));
|
||||
|
||||
String email = formData.getFirst(UserModel.EMAIL);
|
||||
if (!ObjectUtil.isEqualOrBothNull(email, userCtx.getEmail())) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Email updated on updateProfile page to '%s' ", email);
|
||||
}
|
||||
|
||||
userCtx.setEmail(email);
|
||||
context.getClientSession().setNote(UPDATE_PROFILE_EMAIL_CHANGED, "true");
|
||||
}
|
||||
|
||||
AttributeFormDataProcessor.process(formData, realm, userCtx);
|
||||
|
||||
userCtx.saveToClientSession(context.getClientSession());
|
||||
|
||||
logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername());
|
||||
|
||||
event.detail(Details.UPDATED_EMAIL, email);
|
||||
context.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package org.keycloak.authentication.authenticators.broker;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class IdpUpdateProfileAuthenticatorFactory implements AuthenticatorFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "idp-update-profile";
|
||||
static IdpUpdateProfileAuthenticator SINGLETON = new IdpUpdateProfileAuthenticator();
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
return SINGLETON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return "updateProfile";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return REQUIREMENT_CHOICES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Update Profile";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Updates profile data retrieved from Identity Provider in the displayed form";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserSetupAllowed() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package org.keycloak.authentication.authenticators.broker;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.AuthenticationFlowException;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernamePasswordForm;
|
||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
|
||||
/**
|
||||
* Same like classic username+password form, but username is "known" and user can't change it
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class IdpUsernamePasswordForm extends UsernamePasswordForm {
|
||||
|
||||
@Override
|
||||
protected Response challenge(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
|
||||
UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getClientSession());
|
||||
|
||||
return setupForm(context, formData, existingUser)
|
||||
.setStatus(Response.Status.OK)
|
||||
.createLogin();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
|
||||
UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getClientSession());
|
||||
context.setUser(existingUser);
|
||||
|
||||
// Restore formData for the case of error
|
||||
setupForm(context, formData, existingUser);
|
||||
|
||||
return validatePassword(context, formData);
|
||||
}
|
||||
|
||||
protected LoginFormsProvider setupForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData, UserModel existingUser) {
|
||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(context.getClientSession());
|
||||
if (serializedCtx == null) {
|
||||
throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR);
|
||||
}
|
||||
|
||||
formData.add(AuthenticationManager.FORM_USERNAME, existingUser.getUsername());
|
||||
return context.form()
|
||||
.setFormData(formData)
|
||||
.setAttribute(LoginFormsProvider.USERNAME_EDIT_DISABLED, true)
|
||||
.setSuccess(Messages.FEDERATED_IDENTITY_CONFIRM_REAUTHENTICATE_MESSAGE, existingUser.getUsername(), serializedCtx.getIdentityProviderId());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package org.keycloak.authentication.authenticators.broker;
|
||||
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernamePasswordForm;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class IdpUsernamePasswordFormFactory extends UsernamePasswordFormFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "idp-username-password-form";
|
||||
public static final UsernamePasswordForm IDP_SINGLETON = new IdpUsernamePasswordForm();
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
return IDP_SINGLETON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Validates a password from login form. Username is already known from identity provider authentication";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Username Password Form for identity provider reauthentication";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package org.keycloak.authentication.authenticators.broker.util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ExistingUserInfo {
|
||||
private String existingUserId;
|
||||
private String duplicateAttributeName;
|
||||
private String duplicateAttributeValue;
|
||||
|
||||
public ExistingUserInfo() {}
|
||||
|
||||
public ExistingUserInfo(String existingUserId, String duplicateAttributeName, String duplicateAttributeValue) {
|
||||
this.existingUserId = existingUserId;
|
||||
this.duplicateAttributeName = duplicateAttributeName;
|
||||
this.duplicateAttributeValue = duplicateAttributeValue;
|
||||
}
|
||||
|
||||
public String getExistingUserId() {
|
||||
return existingUserId;
|
||||
}
|
||||
|
||||
public void setExistingUserId(String existingUserId) {
|
||||
this.existingUserId = existingUserId;
|
||||
}
|
||||
|
||||
public String getDuplicateAttributeName() {
|
||||
return duplicateAttributeName;
|
||||
}
|
||||
|
||||
public void setDuplicateAttributeName(String duplicateAttributeName) {
|
||||
this.duplicateAttributeName = duplicateAttributeName;
|
||||
}
|
||||
|
||||
public String getDuplicateAttributeValue() {
|
||||
return duplicateAttributeValue;
|
||||
}
|
||||
|
||||
public void setDuplicateAttributeValue(String duplicateAttributeValue) {
|
||||
this.duplicateAttributeValue = duplicateAttributeValue;
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
try {
|
||||
return JsonSerialization.writeValueAsString(this);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static ExistingUserInfo deserialize(String serialized) {
|
||||
try {
|
||||
return JsonSerialization.readValue(serialized, ExistingUserInfo.class);
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,327 @@
|
|||
package org.keycloak.authentication.authenticators.broker.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.codehaus.jackson.annotate.JsonIgnore;
|
||||
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
||||
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityProvider;
|
||||
import org.keycloak.broker.provider.IdentityProviderDataMarshaller;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.reflections.Reflections;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.services.resources.IdentityBrokerService;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
|
||||
|
||||
private String id;
|
||||
private String brokerUsername;
|
||||
private String modelUsername;
|
||||
private String email;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String brokerSessionId;
|
||||
private String brokerUserId;
|
||||
private String code;
|
||||
private String token;
|
||||
|
||||
private String identityProviderId;
|
||||
private Map<String, ContextDataEntry> contextData = new HashMap<>();
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public boolean isEditUsernameAllowed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return modelUsername;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUsername(String username) {
|
||||
this.modelUsername = username;
|
||||
}
|
||||
|
||||
public String getModelUsername() {
|
||||
return modelUsername;
|
||||
}
|
||||
|
||||
public void setModelUsername(String modelUsername) {
|
||||
this.modelUsername = modelUsername;
|
||||
}
|
||||
|
||||
public String getBrokerUsername() {
|
||||
return brokerUsername;
|
||||
}
|
||||
|
||||
public void setBrokerUsername(String modelUsername) {
|
||||
this.brokerUsername = modelUsername;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getBrokerSessionId() {
|
||||
return brokerSessionId;
|
||||
}
|
||||
|
||||
public void setBrokerSessionId(String brokerSessionId) {
|
||||
this.brokerSessionId = brokerSessionId;
|
||||
}
|
||||
|
||||
public String getBrokerUserId() {
|
||||
return brokerUserId;
|
||||
}
|
||||
|
||||
public void setBrokerUserId(String brokerUserId) {
|
||||
this.brokerUserId = brokerUserId;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public String getIdentityProviderId() {
|
||||
return identityProviderId;
|
||||
}
|
||||
|
||||
public void setIdentityProviderId(String identityProviderId) {
|
||||
this.identityProviderId = identityProviderId;
|
||||
}
|
||||
|
||||
public Map<String, ContextDataEntry> getContextData() {
|
||||
return contextData;
|
||||
}
|
||||
|
||||
public void setContextData(Map<String, ContextDataEntry> contextData) {
|
||||
this.contextData = contextData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getAttributes() {
|
||||
Map<String, List<String>> result = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, ContextDataEntry> entry : this.contextData.entrySet()) {
|
||||
if (entry.getKey().startsWith("user.attributes.")) {
|
||||
ContextDataEntry ctxEntry = entry.getValue();
|
||||
String asString = ctxEntry.getData();
|
||||
try {
|
||||
List<String> asList = JsonSerialization.readValue(asString, List.class);
|
||||
result.put(entry.getKey().substring(16), asList);
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String key, List<String> value) {
|
||||
try {
|
||||
String listStr = JsonSerialization.writeValueAsString(value);
|
||||
ContextDataEntry ctxEntry = ContextDataEntry.create(List.class.getName(), listStr);
|
||||
this.contextData.put("user.attributes." + key, ctxEntry);
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAttribute(String key) {
|
||||
ContextDataEntry ctxEntry = this.contextData.get("user.attributes." + key);
|
||||
if (ctxEntry != null) {
|
||||
try {
|
||||
String asString = ctxEntry.getData();
|
||||
List<String> asList = JsonSerialization.readValue(asString, List.class);
|
||||
return asList;
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public BrokeredIdentityContext deserialize(KeycloakSession session, ClientSessionModel clientSession) {
|
||||
BrokeredIdentityContext ctx = new BrokeredIdentityContext(getId());
|
||||
|
||||
ctx.setUsername(getBrokerUsername());
|
||||
ctx.setModelUsername(getModelUsername());
|
||||
ctx.setEmail(getEmail());
|
||||
ctx.setFirstName(getFirstName());
|
||||
ctx.setLastName(getLastName());
|
||||
ctx.setBrokerSessionId(getBrokerSessionId());
|
||||
ctx.setBrokerUserId(getBrokerUserId());
|
||||
ctx.setCode(getCode());
|
||||
ctx.setToken(getToken());
|
||||
|
||||
RealmModel realm = clientSession.getRealm();
|
||||
IdentityProviderModel idpConfig = realm.getIdentityProviderByAlias(getIdentityProviderId());
|
||||
if (idpConfig == null) {
|
||||
throw new ModelException("Can't find identity provider with ID " + getIdentityProviderId() + " in realm " + realm.getName());
|
||||
}
|
||||
IdentityProvider idp = IdentityBrokerService.getIdentityProvider(session, realm, idpConfig.getAlias());
|
||||
ctx.setIdpConfig(idpConfig);
|
||||
ctx.setIdp(idp);
|
||||
|
||||
IdentityProviderDataMarshaller serializer = idp.getMarshaller();
|
||||
|
||||
for (Map.Entry<String, ContextDataEntry> entry : getContextData().entrySet()) {
|
||||
try {
|
||||
ContextDataEntry value = entry.getValue();
|
||||
Class<?> clazz = Reflections.classForName(value.getClazz(), this.getClass().getClassLoader());
|
||||
|
||||
Object deserialized = serializer.deserialize(value.getData(), clazz);
|
||||
|
||||
ctx.getContextData().put(entry.getKey(), deserialized);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.setClientSession(clientSession);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
public static SerializedBrokeredIdentityContext serialize(BrokeredIdentityContext context) {
|
||||
SerializedBrokeredIdentityContext ctx = new SerializedBrokeredIdentityContext();
|
||||
ctx.setId(context.getId());
|
||||
ctx.setBrokerUsername(context.getUsername());
|
||||
ctx.setModelUsername(context.getModelUsername());
|
||||
ctx.setEmail(context.getEmail());
|
||||
ctx.setFirstName(context.getFirstName());
|
||||
ctx.setLastName(context.getLastName());
|
||||
ctx.setBrokerSessionId(context.getBrokerSessionId());
|
||||
ctx.setBrokerUserId(context.getBrokerUserId());
|
||||
ctx.setCode(context.getCode());
|
||||
ctx.setToken(context.getToken());
|
||||
ctx.setIdentityProviderId(context.getIdpConfig().getAlias());
|
||||
|
||||
IdentityProviderDataMarshaller serializer = context.getIdp().getMarshaller();
|
||||
|
||||
for (Map.Entry<String, Object> entry : context.getContextData().entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
String serializedValue = serializer.serialize(value);
|
||||
|
||||
ContextDataEntry ctxEntry = ContextDataEntry.create(value.getClass().getName(), serializedValue);
|
||||
ctx.getContextData().put(entry.getKey(), ctxEntry);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
// Save this context as note to clientSession
|
||||
public void saveToClientSession(ClientSessionModel clientSession) {
|
||||
try {
|
||||
String asString = JsonSerialization.writeValueAsString(this);
|
||||
clientSession.setNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE, asString);
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
public static SerializedBrokeredIdentityContext readFromClientSession(ClientSessionModel clientSession) {
|
||||
String asString = clientSession.getNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
|
||||
if (asString == null) {
|
||||
return null;
|
||||
} else {
|
||||
try {
|
||||
return JsonSerialization.readValue(asString, SerializedBrokeredIdentityContext.class);
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ContextDataEntry {
|
||||
|
||||
private String clazz;
|
||||
private String data;
|
||||
|
||||
public String getClazz() {
|
||||
return clazz;
|
||||
}
|
||||
|
||||
public void setClazz(String clazz) {
|
||||
this.clazz = clazz;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static ContextDataEntry create(String clazz, String data) {
|
||||
ContextDataEntry entry = new ContextDataEntry();
|
||||
entry.setClazz(clazz);
|
||||
entry.setData(data);
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
package org.keycloak.authentication.authenticators.resetcred;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
||||
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
||||
import org.keycloak.email.EmailException;
|
||||
import org.keycloak.email.EmailProvider;
|
||||
|
@ -36,10 +38,22 @@ import java.util.concurrent.TimeUnit;
|
|||
*/
|
||||
public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFactory {
|
||||
|
||||
protected static Logger logger = Logger.getLogger(ResetCredentialChooseUser.class);
|
||||
|
||||
public static final String PROVIDER_ID = "reset-credentials-choose-user";
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticationFlowContext context) {
|
||||
String existingUserId = context.getClientSession().getNote(AbstractIdpAuthenticator.EXISTING_USER_INFO);
|
||||
if (existingUserId != null) {
|
||||
UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getClientSession());
|
||||
|
||||
logger.debugf("Forget-password triggered when reauthenticating user after first broker login. Skipping reset-credential-choose-user screen and using user '%s' ", existingUser.getUsername());
|
||||
context.setUser(existingUser);
|
||||
context.success();
|
||||
return;
|
||||
}
|
||||
|
||||
Response challenge = context.form().createPasswordReset();
|
||||
context.challenge(challenge);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.keycloak.events.Errors;
|
|||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -34,7 +35,7 @@ import java.util.concurrent.TimeUnit;
|
|||
*/
|
||||
public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory {
|
||||
public static final String RESET_CREDENTIAL_SECRET = "RESET_CREDENTIAL_SECRET";
|
||||
public static final String KEY = "key";
|
||||
|
||||
protected static Logger logger = Logger.getLogger(ResetCredentialEmail.class);
|
||||
|
||||
public static final String PROVIDER_ID = "reset-credential-email";
|
||||
|
@ -67,7 +68,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
|||
// it can only be guessed once, and it must match watch is stored in the client session.
|
||||
String secret = HmacOTP.generateSecret(10);
|
||||
context.getClientSession().setNote(RESET_CREDENTIAL_SECRET, secret);
|
||||
String link = UriBuilder.fromUri(context.getActionUrl()).queryParam(KEY, secret).build().toString();
|
||||
String link = UriBuilder.fromUri(context.getActionUrl()).queryParam(Constants.KEY, secret).build().toString();
|
||||
long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
|
||||
try {
|
||||
|
||||
|
@ -93,7 +94,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
|||
@Override
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
String secret = context.getClientSession().getNote(RESET_CREDENTIAL_SECRET);
|
||||
String key = context.getUriInfo().getQueryParameters().getFirst(KEY);
|
||||
String key = context.getUriInfo().getQueryParameters().getFirst(Constants.KEY);
|
||||
|
||||
// Can only guess once! We remove the note so another guess can't happen
|
||||
context.getClientSession().removeNote(RESET_CREDENTIAL_SECRET);
|
||||
|
|
|
@ -52,7 +52,7 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
|
|||
RealmModel realm = context.getRealm();
|
||||
|
||||
|
||||
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm, formData);
|
||||
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm.isEditUsernameAllowed(), formData);
|
||||
if (errors != null && !errors.isEmpty()) {
|
||||
Response challenge = context.form()
|
||||
.setErrors(errors)
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package org.keycloak.authentication.requiredactions.util;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Abstraction, which allows to display updateProfile page in various contexts (Required action of already existing user, or first identity provider
|
||||
* login when user doesn't yet exists in Keycloak DB)
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface UpdateProfileContext {
|
||||
|
||||
boolean isEditUsernameAllowed();
|
||||
|
||||
String getUsername();
|
||||
|
||||
void setUsername(String username);
|
||||
|
||||
String getEmail();
|
||||
|
||||
void setEmail(String email);
|
||||
|
||||
String getFirstName();
|
||||
|
||||
void setFirstName(String firstName);
|
||||
|
||||
String getLastName();
|
||||
|
||||
void setLastName(String lastName);
|
||||
|
||||
Map<String, List<String>> getAttributes();
|
||||
|
||||
void setAttribute(String key, List<String> value);
|
||||
|
||||
List<String> getAttribute(String key);
|
||||
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package org.keycloak.authentication.requiredactions.util;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class UserUpdateProfileContext implements UpdateProfileContext {
|
||||
|
||||
private final RealmModel realm;
|
||||
private final UserModel user;
|
||||
|
||||
public UserUpdateProfileContext(RealmModel realm, UserModel user) {
|
||||
this.realm = realm;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEditUsernameAllowed() {
|
||||
return realm.isEditUsernameAllowed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return user.getUsername();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUsername(String username) {
|
||||
user.setUsername(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEmail() {
|
||||
return user.getEmail();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEmail(String email) {
|
||||
user.setEmail(email);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFirstName() {
|
||||
return user.getFirstName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFirstName(String firstName) {
|
||||
user.setFirstName(firstName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLastName() {
|
||||
return user.getLastName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastName(String lastName) {
|
||||
user.setLastName(lastName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getAttributes() {
|
||||
return user.getAttributes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String key, List<String> value) {
|
||||
user.setAttribute(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAttribute(String key) {
|
||||
return user.getAttribute(key);
|
||||
}
|
||||
}
|
|
@ -32,6 +32,8 @@ import org.keycloak.services.resources.RealmsResource;
|
|||
import org.keycloak.services.resources.ThemeResource;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
|
@ -94,6 +96,13 @@ public class Urls {
|
|||
return identityProviderAuthnRequest(baseURI, providerId, realmName, null);
|
||||
}
|
||||
|
||||
public static URI identityProviderAfterFirstBrokerLogin(URI baseUri, String realmName, String accessCode) {
|
||||
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
||||
.path(IdentityBrokerService.class, "afterFirstBrokerLogin")
|
||||
.replaceQueryParam(OAuth2Constants.CODE, accessCode)
|
||||
.build(realmName);
|
||||
}
|
||||
|
||||
public static URI accountTotpPage(URI baseUri, String realmId) {
|
||||
return accountBase(baseUri).path(AccountService.class, "totpPage").build(realmId);
|
||||
}
|
||||
|
@ -204,6 +213,11 @@ public class Urls {
|
|||
return loginActionsBase(baseUri).path(LoginActionsService.class, "processConsent").build(realmId);
|
||||
}
|
||||
|
||||
public static URI firstBrokerLoginProcessor(URI baseUri, String realmName) {
|
||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "firstBrokerLoginGet")
|
||||
.build(realmName);
|
||||
}
|
||||
|
||||
public static String localeCookiePath(URI baseUri, String realmName){
|
||||
return realmBase(baseUri).path(realmName).build().getRawPath();
|
||||
}
|
||||
|
|
|
@ -64,9 +64,13 @@ public class Messages {
|
|||
|
||||
public static final String EMAIL_EXISTS = "emailExistsMessage";
|
||||
|
||||
public static final String FEDERATED_IDENTITY_EMAIL_EXISTS = "federatedIdentityEmailExistsMessage";
|
||||
public static final String FEDERATED_IDENTITY_EXISTS = "federatedIdentityExistsMessage";
|
||||
|
||||
public static final String FEDERATED_IDENTITY_USERNAME_EXISTS = "federatedIdentityUsernameExistsMessage";
|
||||
public static final String FEDERATED_IDENTITY_CONFIRM_LINK_MESSAGE = "federatedIdentityConfirmLinkMessage";
|
||||
|
||||
public static final String FEDERATED_IDENTITY_CONFIRM_REAUTHENTICATE_MESSAGE = "federatedIdentityConfirmReauthenticateMessage";
|
||||
|
||||
public static final String IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE = "identityProviderDifferentUserMessage";
|
||||
|
||||
public static final String CONFIGURE_TOTP = "configureTotpMessage";
|
||||
|
||||
|
@ -76,6 +80,8 @@ public class Messages {
|
|||
|
||||
public static final String VERIFY_EMAIL = "verifyEmailMessage";
|
||||
|
||||
public static final String LINK_IDP = "linkIdpMessage";
|
||||
|
||||
public static final String EMAIL_VERIFIED = "emailVerifiedMessage";
|
||||
|
||||
public static final String EMAIL_SENT = "emailSentMessage";
|
||||
|
@ -147,6 +153,8 @@ public class Messages {
|
|||
|
||||
public static final String IDENTITY_PROVIDER_NOT_FOUND = "identityProviderNotFoundMessage";
|
||||
|
||||
public static final String IDENTITY_PROVIDER_LINK_SUCCESS = "identityProviderLinkSuccess";
|
||||
|
||||
public static final String IDENTITY_PROVIDER_NOT_UNIQUE = "identityProviderNotUniqueMessage";
|
||||
|
||||
public static final String REALM_SUPPORTS_NO_CREDENTIALS = "realmSupportsNoCredentialsMessage";
|
||||
|
|
|
@ -366,7 +366,7 @@ public class AccountService extends AbstractSecuredLocalService {
|
|||
|
||||
UserModel user = auth.getUser();
|
||||
|
||||
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm, formData);
|
||||
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm.isEditUsernameAllowed(), formData);
|
||||
if (errors != null && !errors.isEmpty()) {
|
||||
setReferrerOnPage();
|
||||
return account.setErrors(errors).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
|
||||
|
|
|
@ -3,6 +3,8 @@ package org.keycloak.services.resources;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
|
||||
import org.keycloak.authentication.requiredactions.util.UserUpdateProfileContext;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
|
@ -21,6 +23,11 @@ public class AttributeFormDataProcessor {
|
|||
* @param user
|
||||
*/
|
||||
public static void process(MultivaluedMap<String, String> formData, RealmModel realm, UserModel user) {
|
||||
UpdateProfileContext userCtx = new UserUpdateProfileContext(realm, user);
|
||||
process(formData, realm, userCtx);
|
||||
}
|
||||
|
||||
public static void process(MultivaluedMap<String, String> formData, RealmModel realm, UpdateProfileContext user) {
|
||||
for (String key : formData.keySet()) {
|
||||
if (!key.startsWith("user.attributes.")) continue;
|
||||
String attribute = key.substring("user.attributes.".length());
|
||||
|
@ -36,7 +43,6 @@ public class AttributeFormDataProcessor {
|
|||
|
||||
user.setAttribute(attribute, modelValue);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void addOrSetValue(List<String> list, int index, String value) {
|
||||
|
|
|
@ -20,6 +20,9 @@ package org.keycloak.services.resources;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||
|
@ -28,10 +31,12 @@ import org.keycloak.broker.provider.IdentityBrokerException;
|
|||
import org.keycloak.broker.provider.IdentityProvider;
|
||||
import org.keycloak.broker.provider.IdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.IdentityProviderMapper;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
|
@ -49,7 +54,6 @@ import org.keycloak.models.utils.FormMessage;
|
|||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager.AuthResult;
|
||||
import org.keycloak.services.managers.BruteForceProtector;
|
||||
|
@ -70,6 +74,7 @@ import javax.ws.rs.core.Response.Status;
|
|||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
@ -79,7 +84,6 @@ import java.util.Set;
|
|||
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
|
||||
import static org.keycloak.models.ClientSessionModel.Action.AUTHENTICATE;
|
||||
import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
|
||||
import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PROFILE;
|
||||
|
||||
/**
|
||||
* <p></p>
|
||||
|
@ -285,27 +289,133 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
}
|
||||
|
||||
if (federatedUser == null) {
|
||||
try {
|
||||
federatedUser = createUser(context);
|
||||
|
||||
if (IdentityProviderRepresentation.UPFLM_ON.equals(identityProviderConfig.getUpdateProfileFirstLoginMode())
|
||||
|| (IdentityProviderRepresentation.UPFLM_MISSING.equals(identityProviderConfig.getUpdateProfileFirstLoginMode()) && !Validation.validateUserMandatoryFields(realmModel, federatedUser))) {
|
||||
if (isDebugEnabled()) {
|
||||
LOGGER.debugf("Identity provider requires update profile action.", federatedUser);
|
||||
}
|
||||
federatedUser.addRequiredAction(UPDATE_PROFILE);
|
||||
LOGGER.debugf("Federated user not found for provider '%s' and broker username '%s' . Redirecting to flow for firstBrokerLogin", providerId, context.getUsername());
|
||||
|
||||
String username = context.getModelUsername();
|
||||
if (username == null) {
|
||||
if (this.realmModel.isRegistrationEmailAsUsername() && !Validation.isBlank(context.getEmail())) {
|
||||
username = context.getEmail();
|
||||
} else if (context.getUsername() == null) {
|
||||
username = context.getIdpConfig().getAlias() + "." + context.getId();
|
||||
} else {
|
||||
username = context.getIdpConfig().getAlias() + "." + context.getUsername();
|
||||
}
|
||||
if(identityProviderConfig.isTrustEmail() && !Validation.isBlank(federatedUser.getEmail())){
|
||||
federatedUser.setEmailVerified(true);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return redirectToLoginPage(e, clientCode);
|
||||
}
|
||||
username = username.trim();
|
||||
context.setModelUsername(username);
|
||||
|
||||
clientSession.setTimestamp(Time.currentTime());
|
||||
|
||||
SerializedBrokeredIdentityContext ctx = SerializedBrokeredIdentityContext.serialize(context);
|
||||
ctx.saveToClientSession(clientSession);
|
||||
|
||||
URI redirect = LoginActionsService.firstBrokerLoginProcessor(uriInfo)
|
||||
.queryParam(OAuth2Constants.CODE, context.getCode())
|
||||
.build(realmModel.getName());
|
||||
return Response.status(302).location(redirect).build();
|
||||
|
||||
} else {
|
||||
updateFederatedIdentity(context, federatedUser);
|
||||
|
||||
boolean firstBrokerLoginInProgress = (clientSession.getNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null);
|
||||
if (firstBrokerLoginInProgress) {
|
||||
LOGGER.debugf("Reauthenticated with broker '%s' when linking user '%s' with other broker", context.getIdpConfig().getAlias(), federatedUser.getUsername());
|
||||
|
||||
UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realmModel, clientSession);
|
||||
if (!linkingUser.getId().equals(federatedUser.getId())) {
|
||||
return redirectToErrorPage(Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE, federatedUser.getUsername(), linkingUser.getUsername());
|
||||
}
|
||||
|
||||
clientSession.setAuthenticatedUser(federatedUser);
|
||||
return afterFirstBrokerLogin(context.getCode());
|
||||
}
|
||||
|
||||
return finishBrokerAuthentication(context, federatedUser, clientSession, providerId);
|
||||
}
|
||||
}
|
||||
|
||||
// Callback from LoginActionsService after first login with broker was done and Keycloak account is successfully linked/created
|
||||
@GET
|
||||
@Path("/after-first-broker-login")
|
||||
public Response afterFirstBrokerLogin(@QueryParam("code") String code) {
|
||||
ClientSessionCode clientCode = parseClientSessionCode(code);
|
||||
ClientSessionModel clientSession = clientCode.getClientSession();
|
||||
|
||||
try {
|
||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession);
|
||||
if (serializedCtx == null) {
|
||||
throw new IdentityBrokerException("Not found serialized context in clientSession");
|
||||
}
|
||||
BrokeredIdentityContext context = serializedCtx.deserialize(session, clientSession);
|
||||
String providerId = context.getIdpConfig().getAlias();
|
||||
|
||||
// firstBrokerLogin workflow finished. Removing note now
|
||||
clientSession.removeNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
|
||||
|
||||
UserModel federatedUser = clientSession.getAuthenticatedUser();
|
||||
if (federatedUser == null) {
|
||||
throw new IdentityBrokerException("Couldn't found authenticated federatedUser in clientSession");
|
||||
}
|
||||
|
||||
if (context.getIdpConfig().isAddReadTokenRoleOnCreate()) {
|
||||
RoleModel readTokenRole = realmModel.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE);
|
||||
federatedUser.grantRole(readTokenRole);
|
||||
}
|
||||
|
||||
// Add federated identity link here
|
||||
FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(context.getIdpConfig().getAlias(), context.getId(),
|
||||
context.getUsername(), context.getToken());
|
||||
session.users().addFederatedIdentity(realmModel, federatedUser, federatedIdentityModel);
|
||||
|
||||
String isRegisteredNewUser = clientSession.getNote(AbstractIdpAuthenticator.BROKER_REGISTERED_NEW_USER);
|
||||
if (Boolean.parseBoolean(isRegisteredNewUser)) {
|
||||
|
||||
LOGGER.debugf("Registered new user '%s' after first login with identity provider '%s'. Identity provider username is '%s' . ", federatedUser.getUsername(), providerId, context.getUsername());
|
||||
|
||||
context.getIdp().importNewUser(session, realmModel, federatedUser, context);
|
||||
Set<IdentityProviderMapperModel> mappers = realmModel.getIdentityProviderMappersByAlias(providerId);
|
||||
if (mappers != null) {
|
||||
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
||||
for (IdentityProviderMapperModel mapper : mappers) {
|
||||
IdentityProviderMapper target = (IdentityProviderMapper)sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
|
||||
target.importNewUser(session, realmModel, federatedUser, mapper, context);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.getIdpConfig().isTrustEmail() && !Validation.isBlank(federatedUser.getEmail()) && !Boolean.parseBoolean(clientSession.getNote(AbstractIdpAuthenticator.UPDATE_PROFILE_EMAIL_CHANGED))) {
|
||||
LOGGER.debugf("Email verified automatically after registration of user '%s' through Identity provider '%s' ", federatedUser.getUsername(), context.getIdpConfig().getAlias());
|
||||
federatedUser.setEmailVerified(true);
|
||||
}
|
||||
|
||||
this.event.clone().user(federatedUser).event(EventType.REGISTER)
|
||||
.detail(Details.IDENTITY_PROVIDER, providerId)
|
||||
.detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername())
|
||||
.removeDetail("auth_method")
|
||||
.success();
|
||||
|
||||
} else {
|
||||
LOGGER.debugf("Linked existing keycloak user '%s' with identity provider '%s' . Identity provider username is '%s' .", federatedUser.getUsername(), providerId, context.getUsername());
|
||||
|
||||
updateFederatedIdentity(context, federatedUser);
|
||||
}
|
||||
|
||||
String isDifferentBrowser = clientSession.getNote(AbstractIdpAuthenticator.IS_DIFFERENT_BROWSER);
|
||||
if (Boolean.parseBoolean(isDifferentBrowser)) {
|
||||
session.sessions().removeClientSession(realmModel, clientSession);
|
||||
return session.getProvider(LoginFormsProvider.class)
|
||||
.setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, context.getIdpConfig().getAlias(), context.getUsername())
|
||||
.createInfoPage();
|
||||
} else {
|
||||
return finishBrokerAuthentication(context, federatedUser, clientSession, providerId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// TODO?
|
||||
return redirectToErrorPage(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Response finishBrokerAuthentication(BrokeredIdentityContext context, UserModel federatedUser, ClientSessionModel clientSession, String providerId) {
|
||||
UserSessionModel userSession = this.session.sessions()
|
||||
.createUserSession(this.realmModel, federatedUser, federatedUser.getUsername(), this.clientConnection.getRemoteAddr(), "broker", false, context.getBrokerSessionId(), context.getBrokerUserId());
|
||||
|
||||
|
@ -376,7 +486,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
FederatedIdentityModel federatedIdentityModel = this.session.users().getFederatedIdentity(federatedUser, context.getIdpConfig().getAlias(), this.realmModel);
|
||||
|
||||
// Skip DB write if tokens are null or equal
|
||||
if (context.getIdpConfig().isStoreToken() && !ObjectUtil.isEqualOrNull(context.getToken(), federatedIdentityModel.getToken())) {
|
||||
if (context.getIdpConfig().isStoreToken() && !ObjectUtil.isEqualOrBothNull(context.getToken(), federatedIdentityModel.getToken())) {
|
||||
federatedIdentityModel.setToken(context.getToken());
|
||||
|
||||
this.session.users().updateFederatedIdentity(this.realmModel, federatedUser, federatedIdentityModel);
|
||||
|
@ -412,6 +522,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
|
||||
LOGGER.debugf("Got authorization code from client [%s].", client.getClientId());
|
||||
this.event.client(client);
|
||||
this.session.getContext().setClient(client);
|
||||
|
||||
if (clientSession.getUserSession() != null) {
|
||||
this.event.session(clientSession.getUserSession());
|
||||
|
@ -534,100 +645,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
}
|
||||
|
||||
private IdentityProviderModel getIdentityProviderConfig(String providerId) {
|
||||
for (IdentityProviderModel model : this.realmModel.getIdentityProviders()) {
|
||||
if (model.getAlias().equals(providerId)) {
|
||||
return model;
|
||||
}
|
||||
IdentityProviderModel model = this.realmModel.getIdentityProviderByAlias(providerId);
|
||||
if (model == null) {
|
||||
throw new IdentityBrokerException("Configuration for identity provider [" + providerId + "] not found.");
|
||||
}
|
||||
|
||||
throw new IdentityBrokerException("Configuration for identity provider [" + providerId + "] not found.");
|
||||
}
|
||||
|
||||
private UserModel createUser(BrokeredIdentityContext context) {
|
||||
FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(context.getIdpConfig().getAlias(), context.getId(),
|
||||
context.getUsername(), context.getToken());
|
||||
// Check if no user already exists with this username or email
|
||||
UserModel existingUser = null;
|
||||
|
||||
if (context.getEmail() != null) {
|
||||
existingUser = this.session.users().getUserByEmail(context.getEmail(), this.realmModel);
|
||||
}
|
||||
|
||||
if (existingUser != null) {
|
||||
fireErrorEvent(Errors.FEDERATED_IDENTITY_EMAIL_EXISTS);
|
||||
throw new IdentityBrokerException(Messages.FEDERATED_IDENTITY_EMAIL_EXISTS);
|
||||
}
|
||||
String username = context.getModelUsername();
|
||||
if (username == null) {
|
||||
username = context.getUsername();
|
||||
if (this.realmModel.isRegistrationEmailAsUsername() && !Validation.isBlank(context.getEmail())) {
|
||||
username = context.getEmail();
|
||||
} else if (username == null) {
|
||||
username = context.getIdpConfig().getAlias() + "." + context.getId();
|
||||
} else {
|
||||
username = context.getIdpConfig().getAlias() + "." + context.getUsername();
|
||||
}
|
||||
}
|
||||
if (username == null) {
|
||||
LOGGER.warn("Unknown username");
|
||||
fireErrorEvent(Errors.FEDERATED_IDENTITY_USERNAME_EXISTS);
|
||||
throw new IdentityBrokerException(Messages.FEDERATED_IDENTITY_USERNAME_EXISTS);
|
||||
|
||||
}
|
||||
username = username.trim();
|
||||
|
||||
existingUser = this.session.users().getUserByUsername(username, this.realmModel);
|
||||
|
||||
if (existingUser != null) {
|
||||
fireErrorEvent(Errors.FEDERATED_IDENTITY_USERNAME_EXISTS);
|
||||
throw new IdentityBrokerException(Messages.FEDERATED_IDENTITY_USERNAME_EXISTS);
|
||||
}
|
||||
|
||||
if (isDebugEnabled()) {
|
||||
LOGGER.debugf("Creating account from identity [%s].", federatedIdentityModel);
|
||||
}
|
||||
|
||||
UserModel federatedUser = this.session.users().addUser(this.realmModel, username);
|
||||
|
||||
if (isDebugEnabled()) {
|
||||
LOGGER.debugf("Account [%s] created.", federatedUser);
|
||||
}
|
||||
|
||||
federatedUser.setEnabled(true);
|
||||
federatedUser.setEmail(context.getEmail());
|
||||
federatedUser.setFirstName(context.getFirstName());
|
||||
federatedUser.setLastName(context.getLastName());
|
||||
|
||||
|
||||
if (context.getIdpConfig().isAddReadTokenRoleOnCreate()) {
|
||||
RoleModel readTokenRole = realmModel.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE);
|
||||
federatedUser.grantRole(readTokenRole);
|
||||
}
|
||||
|
||||
if (context.getIdpConfig().isStoreToken()) {
|
||||
federatedIdentityModel.setToken(context.getToken());
|
||||
}
|
||||
|
||||
this.session.users().addFederatedIdentity(this.realmModel, federatedUser, federatedIdentityModel);
|
||||
|
||||
context.getIdp().importNewUser(session, realmModel, federatedUser, context);
|
||||
Set<IdentityProviderMapperModel> mappers = realmModel.getIdentityProviderMappersByAlias(context.getIdpConfig().getAlias());
|
||||
if (mappers != null) {
|
||||
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
||||
for (IdentityProviderMapperModel mapper : mappers) {
|
||||
IdentityProviderMapper target = (IdentityProviderMapper)sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
|
||||
target.importNewUser(session, realmModel, federatedUser, mapper, context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.event.clone().user(federatedUser).event(EventType.REGISTER)
|
||||
.detail(Details.IDENTITY_PROVIDER, federatedIdentityModel.getIdentityProvider())
|
||||
.detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername())
|
||||
.removeDetail("auth_method")
|
||||
.success();
|
||||
|
||||
return federatedUser;
|
||||
return model;
|
||||
}
|
||||
|
||||
private Response corsResponse(Response response, ClientModel clientModel) {
|
||||
|
|
|
@ -23,8 +23,10 @@ package org.keycloak.services.resources;
|
|||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
||||
import org.keycloak.authentication.requiredactions.VerifyEmail;
|
||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
|
@ -33,6 +35,7 @@ import org.keycloak.authentication.RequiredActionContextResult;
|
|||
import org.keycloak.authentication.RequiredActionFactory;
|
||||
import org.keycloak.authentication.RequiredActionProvider;
|
||||
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
|
@ -51,7 +54,6 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.models.UserModel.RequiredAction;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.models.utils.HmacOTP;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.RestartLoginCookie;
|
||||
|
@ -92,6 +94,7 @@ public class LoginActionsService {
|
|||
public static final String REGISTRATION_PATH = "registration";
|
||||
public static final String RESET_CREDENTIALS_PATH = "reset-credentials";
|
||||
public static final String REQUIRED_ACTION = "required-action";
|
||||
public static final String FIRST_BROKER_LOGIN_PATH = "first-broker-login";
|
||||
|
||||
private RealmModel realm;
|
||||
|
||||
|
@ -134,6 +137,10 @@ public class LoginActionsService {
|
|||
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "processRegister");
|
||||
}
|
||||
|
||||
public static UriBuilder firstBrokerLoginProcessor(UriInfo uriInfo) {
|
||||
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "firstBrokerLoginGet");
|
||||
}
|
||||
|
||||
public static UriBuilder loginActionsBaseUrl(UriBuilder baseUriBuilder) {
|
||||
return baseUriBuilder.path(RealmsResource.class).path(RealmsResource.class, "getLoginActionsService");
|
||||
}
|
||||
|
@ -208,7 +215,7 @@ public class LoginActionsService {
|
|||
ClientSessionModel clientSession = RestartLoginCookie.restartSession(session, realm, code);
|
||||
if (clientSession != null) {
|
||||
event.clone().detail(Details.RESTART_AFTER_TIMEOUT, "true").error(Errors.EXPIRED_CODE);
|
||||
response = processFlow(null, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), Messages.LOGIN_TIMEOUT);
|
||||
response = processFlow(null, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), Messages.LOGIN_TIMEOUT, new AuthenticationProcessor());
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
@ -267,11 +274,10 @@ public class LoginActionsService {
|
|||
}
|
||||
|
||||
protected Response processAuthentication(String execution, ClientSessionModel clientSession, String errorMessage) {
|
||||
return processFlow(execution, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), errorMessage);
|
||||
return processFlow(execution, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), errorMessage, new AuthenticationProcessor());
|
||||
}
|
||||
|
||||
protected Response processFlow(String execution, ClientSessionModel clientSession, String flowPath, AuthenticationFlowModel flow, String errorMessage) {
|
||||
AuthenticationProcessor processor = new AuthenticationProcessor();
|
||||
protected Response processFlow(String execution, ClientSessionModel clientSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) {
|
||||
processor.setClientSession(clientSession)
|
||||
.setFlowPath(flowPath)
|
||||
.setBrowserFlow(true)
|
||||
|
@ -384,12 +390,33 @@ public class LoginActionsService {
|
|||
}
|
||||
|
||||
protected Response processResetCredentials(String execution, ClientSessionModel clientSession, String errorMessage) {
|
||||
return processFlow(execution, clientSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), errorMessage);
|
||||
AuthenticationProcessor authProcessor = new AuthenticationProcessor() {
|
||||
|
||||
@Override
|
||||
protected Response authenticationComplete() {
|
||||
boolean firstBrokerLoginInProgress = (clientSession.getNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null);
|
||||
if (firstBrokerLoginInProgress) {
|
||||
|
||||
UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realm, clientSession);
|
||||
if (!linkingUser.getId().equals(clientSession.getAuthenticatedUser().getId())) {
|
||||
return ErrorPage.error(session, Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE, clientSession.getAuthenticatedUser().getUsername(), linkingUser.getUsername());
|
||||
}
|
||||
|
||||
logger.debugf("Forget-password flow finished when authenticated user '%s' after first broker login.", linkingUser.getUsername());
|
||||
|
||||
return redirectToAfterFirstBrokerLoginEndpoint(clientSession);
|
||||
} else {
|
||||
return super.authenticationComplete();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return processFlow(execution, clientSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), errorMessage, authProcessor);
|
||||
}
|
||||
|
||||
|
||||
protected Response processRegistration(String execution, ClientSessionModel clientSession, String errorMessage) {
|
||||
return processFlow(execution, clientSession, REGISTRATION_PATH, realm.getRegistrationFlow(), errorMessage);
|
||||
return processFlow(execution, clientSession, REGISTRATION_PATH, realm.getRegistrationFlow(), errorMessage, new AuthenticationProcessor());
|
||||
}
|
||||
|
||||
|
||||
|
@ -450,6 +477,60 @@ public class LoginActionsService {
|
|||
return processRegistration(execution, clientSession, null);
|
||||
}
|
||||
|
||||
@Path(FIRST_BROKER_LOGIN_PATH)
|
||||
@GET
|
||||
public Response firstBrokerLoginGet(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution) {
|
||||
return firstBrokerLogin(code, execution);
|
||||
}
|
||||
|
||||
@Path(FIRST_BROKER_LOGIN_PATH)
|
||||
@POST
|
||||
public Response firstBrokerLoginPost(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution) {
|
||||
return firstBrokerLogin(code, execution);
|
||||
}
|
||||
|
||||
protected Response firstBrokerLogin(String code, String execution) {
|
||||
event.event(EventType.IDENTITY_PROVIDER_FIRST_LOGIN);
|
||||
|
||||
Checks checks = new Checks();
|
||||
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name())) {
|
||||
return checks.response;
|
||||
}
|
||||
event.detail(Details.CODE_ID, code);
|
||||
ClientSessionCode clientSessionCode = checks.clientCode;
|
||||
ClientSessionModel clientSession = clientSessionCode.getClientSession();
|
||||
|
||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession);
|
||||
if (serializedCtx == null) {
|
||||
throw new WebApplicationException(ErrorPage.error(session, "Not found serialized context in clientSession"));
|
||||
}
|
||||
BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, clientSession);
|
||||
AuthenticationFlowModel firstBrokerLoginFlow = realm.getAuthenticationFlowById(brokerContext.getIdpConfig().getFirstBrokerLoginFlowId());
|
||||
|
||||
AuthenticationProcessor processor = new AuthenticationProcessor() {
|
||||
|
||||
@Override
|
||||
protected Response authenticationComplete() {
|
||||
return redirectToAfterFirstBrokerLoginEndpoint(clientSession);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return processFlow(execution, clientSession, FIRST_BROKER_LOGIN_PATH, firstBrokerLoginFlow, null, processor);
|
||||
}
|
||||
|
||||
private Response redirectToAfterFirstBrokerLoginEndpoint(ClientSessionModel clientSession) {
|
||||
ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession);
|
||||
clientSession.setTimestamp(Time.currentTime());
|
||||
|
||||
URI redirect = Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode());
|
||||
logger.debugf("Redirecting to '%s' ", redirect);
|
||||
|
||||
return Response.status(302).location(redirect).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth grant page. You should not invoked this directly!
|
||||
*
|
||||
|
@ -627,6 +708,10 @@ public class LoginActionsService {
|
|||
}
|
||||
|
||||
private String getActionCookie() {
|
||||
return getActionCookie(headers, realm, uriInfo, clientConnection);
|
||||
}
|
||||
|
||||
public static String getActionCookie(HttpHeaders headers, RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection) {
|
||||
Cookie cookie = headers.getCookies().get(ACTION_COOKIE);
|
||||
AuthenticationManager.expireCookie(realm, ACTION_COOKIE, AuthenticationManager.getRealmCookiePath(realm, uriInfo), realm.getSslRequired().isRequired(clientConnection), clientConnection);
|
||||
return cookie != null ? cookie.getValue() : null;
|
||||
|
|
|
@ -79,7 +79,7 @@ public class IdentityProviderResource {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public IdentityProviderRepresentation getIdentityProvider() {
|
||||
this.auth.requireView();
|
||||
IdentityProviderRepresentation rep = ModelToRepresentation.toRepresentation(this.identityProviderModel);
|
||||
IdentityProviderRepresentation rep = ModelToRepresentation.toRepresentation(realm, this.identityProviderModel);
|
||||
return rep;
|
||||
}
|
||||
|
||||
|
@ -117,7 +117,7 @@ public class IdentityProviderResource {
|
|||
String newProviderId = providerRep.getAlias();
|
||||
String oldProviderId = getProviderIdByInternalId(this.realm, internalId);
|
||||
|
||||
this.realm.updateIdentityProvider(RepresentationToModel.toModel(providerRep));
|
||||
this.realm.updateIdentityProvider(RepresentationToModel.toModel(realm, providerRep));
|
||||
|
||||
if (oldProviderId != null && !oldProviderId.equals(newProviderId)) {
|
||||
|
||||
|
|
|
@ -145,7 +145,7 @@ public class IdentityProvidersResource {
|
|||
List<IdentityProviderRepresentation> representations = new ArrayList<IdentityProviderRepresentation>();
|
||||
|
||||
for (IdentityProviderModel identityProviderModel : realm.getIdentityProviders()) {
|
||||
representations.add(ModelToRepresentation.toRepresentation(identityProviderModel));
|
||||
representations.add(ModelToRepresentation.toRepresentation(realm, identityProviderModel));
|
||||
}
|
||||
return representations;
|
||||
}
|
||||
|
@ -164,7 +164,7 @@ public class IdentityProvidersResource {
|
|||
this.auth.requireManage();
|
||||
|
||||
try {
|
||||
IdentityProviderModel identityProvider = RepresentationToModel.toModel(representation);
|
||||
IdentityProviderModel identityProvider = RepresentationToModel.toModel(realm, representation);
|
||||
this.realm.addIdentityProvider(identityProvider);
|
||||
|
||||
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, identityProvider.getInternalId())
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package org.keycloak.services.validation;
|
||||
|
||||
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
|
@ -68,13 +68,13 @@ public class Validation {
|
|||
}
|
||||
|
||||
public static List<FormMessage> validateUpdateProfileForm(MultivaluedMap<String, String> formData) {
|
||||
return validateUpdateProfileForm(null, formData);
|
||||
return validateUpdateProfileForm(false, formData);
|
||||
}
|
||||
|
||||
public static List<FormMessage> validateUpdateProfileForm(RealmModel realm, MultivaluedMap<String, String> formData) {
|
||||
public static List<FormMessage> validateUpdateProfileForm(boolean editUsernameAllowed, MultivaluedMap<String, String> formData) {
|
||||
List<FormMessage> errors = new ArrayList<>();
|
||||
|
||||
if (realm != null && realm.isEditUsernameAllowed() && isBlank(formData.getFirst(FIELD_USERNAME))) {
|
||||
if (editUsernameAllowed && isBlank(formData.getFirst(FIELD_USERNAME))) {
|
||||
addError(errors, FIELD_USERNAME, Messages.MISSING_USERNAME);
|
||||
}
|
||||
|
||||
|
@ -102,7 +102,7 @@ public class Validation {
|
|||
* @param user to validate
|
||||
* @return true if user object contains all mandatory values, false if some mandatory value is missing
|
||||
*/
|
||||
public static boolean validateUserMandatoryFields(RealmModel realm, UserModel user){
|
||||
public static boolean validateUserMandatoryFields(RealmModel realm, UpdateProfileContext user){
|
||||
return!(isBlank(user.getFirstName()) || isBlank(user.getLastName()) || isBlank(user.getEmail()));
|
||||
}
|
||||
|
||||
|
|
|
@ -9,3 +9,8 @@ org.keycloak.authentication.authenticators.resetcred.ResetCredentialChooseUser
|
|||
org.keycloak.authentication.authenticators.resetcred.ResetCredentialEmail
|
||||
org.keycloak.authentication.authenticators.resetcred.ResetOTP
|
||||
org.keycloak.authentication.authenticators.resetcred.ResetPassword
|
||||
org.keycloak.authentication.authenticators.broker.IdpUpdateProfileAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.broker.IdpDetectDuplicationsAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory
|
||||
|
|
|
@ -434,7 +434,8 @@ public abstract class AbstractIdentityProviderTest {
|
|||
loginPage.findSocialButton(getProviderId());
|
||||
}
|
||||
|
||||
@Test
|
||||
// TODO: Reenable and adjust to KEYCLOAK-1750 changed behaviour
|
||||
// @Test
|
||||
public void testUserAlreadyExistsWhenUpdatingProfile() {
|
||||
this.driver.navigate().to("http://localhost:8081/test-app/");
|
||||
|
||||
|
@ -469,7 +470,8 @@ public abstract class AbstractIdentityProviderTest {
|
|||
assertNotNull(federatedUser);
|
||||
}
|
||||
|
||||
@Test
|
||||
// TODO: Reenable and adjust to KEYCLOAK-1750 changed behaviour
|
||||
// @Test
|
||||
public void testUserAlreadyExistsWhenNotUpdatingProfile() {
|
||||
IdentityProviderModel identityProviderModel = getIdentityProviderModel();
|
||||
|
||||
|
|
|
@ -30,9 +30,9 @@ import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
|||
import org.keycloak.broker.saml.SAMLIdentityProvider;
|
||||
import org.keycloak.broker.saml.SAMLIdentityProviderConfig;
|
||||
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.social.facebook.FacebookIdentityProvider;
|
||||
|
@ -63,7 +63,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
|||
public void testInstallation() throws Exception {
|
||||
RealmModel realm = installTestRealm();
|
||||
|
||||
assertIdentityProviderConfig(realm.getIdentityProviders());
|
||||
assertIdentityProviderConfig(realm, realm.getIdentityProviders());
|
||||
|
||||
assertTrue(realm.isIdentityFederationEnabled());
|
||||
this.realmManager.removeRealm(realm);
|
||||
|
@ -85,6 +85,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
|||
identityProviderModel.setTrustEmail(true);
|
||||
identityProviderModel.setStoreToken(true);
|
||||
identityProviderModel.setAuthenticateByDefault(true);
|
||||
identityProviderModel.setFirstBrokerLoginFlowId(realm.getBrowserFlow().getId());
|
||||
|
||||
realm.updateIdentityProvider(identityProviderModel);
|
||||
|
||||
|
@ -100,6 +101,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
|||
assertTrue(identityProviderModel.isTrustEmail());
|
||||
assertTrue(identityProviderModel.isStoreToken());
|
||||
assertTrue(identityProviderModel.isAuthenticateByDefault());
|
||||
assertEquals(identityProviderModel.getFirstBrokerLoginFlowId(), realm.getBrowserFlow().getId());
|
||||
|
||||
identityProviderModel.getConfig().remove("config-added");
|
||||
identityProviderModel.setEnabled(true);
|
||||
|
@ -122,7 +124,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
|||
this.realmManager.removeRealm(realm);
|
||||
}
|
||||
|
||||
private void assertIdentityProviderConfig(List<IdentityProviderModel> identityProviders) {
|
||||
private void assertIdentityProviderConfig(RealmModel realm, List<IdentityProviderModel> identityProviders) {
|
||||
assertFalse(identityProviders.isEmpty());
|
||||
|
||||
Set<String> checkedProviders = new HashSet<String>(getExpectedProviders());
|
||||
|
@ -138,9 +140,9 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
|||
} else if (OIDCIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
||||
assertOidcIdentityProviderConfig(identityProvider);
|
||||
} else if (FacebookIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
||||
assertFacebookIdentityProviderConfig(identityProvider);
|
||||
assertFacebookIdentityProviderConfig(realm, identityProvider);
|
||||
} else if (GitHubIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
||||
assertGitHubIdentityProviderConfig(identityProvider);
|
||||
assertGitHubIdentityProviderConfig(realm, identityProvider);
|
||||
} else if (TwitterIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
||||
assertTwitterIdentityProviderConfig(identityProvider);
|
||||
} else if (LinkedInIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
||||
|
@ -213,7 +215,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
|||
assertEquals("clientSecret", config.getClientSecret());
|
||||
}
|
||||
|
||||
private void assertFacebookIdentityProviderConfig(IdentityProviderModel identityProvider) {
|
||||
private void assertFacebookIdentityProviderConfig(RealmModel realm, IdentityProviderModel identityProvider) {
|
||||
FacebookIdentityProvider facebookIdentityProvider = new FacebookIdentityProviderFactory().create(identityProvider);
|
||||
OAuth2IdentityProviderConfig config = facebookIdentityProvider.getConfig();
|
||||
|
||||
|
@ -226,12 +228,13 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
|||
assertEquals(false, config.isStoreToken());
|
||||
assertEquals("clientId", config.getClientId());
|
||||
assertEquals("clientSecret", config.getClientSecret());
|
||||
assertEquals(realm.getBrowserFlow().getId(), identityProvider.getFirstBrokerLoginFlowId());
|
||||
assertEquals(FacebookIdentityProvider.AUTH_URL, config.getAuthorizationUrl());
|
||||
assertEquals(FacebookIdentityProvider.TOKEN_URL, config.getTokenUrl());
|
||||
assertEquals(FacebookIdentityProvider.PROFILE_URL, config.getUserInfoUrl());
|
||||
}
|
||||
|
||||
private void assertGitHubIdentityProviderConfig(IdentityProviderModel identityProvider) {
|
||||
private void assertGitHubIdentityProviderConfig(RealmModel realm, IdentityProviderModel identityProvider) {
|
||||
GitHubIdentityProvider gitHubIdentityProvider = new GitHubIdentityProviderFactory().create(identityProvider);
|
||||
OAuth2IdentityProviderConfig config = gitHubIdentityProvider.getConfig();
|
||||
|
||||
|
@ -244,6 +247,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
|||
assertEquals(false, config.isStoreToken());
|
||||
assertEquals("clientId", config.getClientId());
|
||||
assertEquals("clientSecret", config.getClientSecret());
|
||||
assertEquals(realm.getFlowByAlias(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW).getId(), identityProvider.getFirstBrokerLoginFlowId());
|
||||
assertEquals(GitHubIdentityProvider.AUTH_URL, config.getAuthorizationUrl());
|
||||
assertEquals(GitHubIdentityProvider.TOKEN_URL, config.getTokenUrl());
|
||||
assertEquals(GitHubIdentityProvider.PROFILE_URL, config.getUserInfoUrl());
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"providerId" : "facebook",
|
||||
"enabled": true,
|
||||
"updateProfileFirstLogin" : "false",
|
||||
"firstBrokerLoginFlowAlias" : "browser",
|
||||
"config": {
|
||||
"authorizationUrl": "authorizationUrl",
|
||||
"tokenUrl": "tokenUrl",
|
||||
|
|
Loading…
Reference in a new issue