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) {
|
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);
|
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>
|
<artifactId>jboss-logging</artifactId>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</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.AuthenticationRequest;
|
||||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||||
|
import org.keycloak.broker.provider.IdentityProviderDataMarshaller;
|
||||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||||
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
||||||
|
@ -263,4 +264,8 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||||
return SignatureAlgorithm.RSA_SHA256;
|
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
|
* @param str2
|
||||||
* @return true if both strings are null or equal
|
* @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) {
|
if (str1 == null && str2 == null) {
|
||||||
return true;
|
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.4.0.xml"/>
|
||||||
<include file="META-INF/jpa-changelog-1.5.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.6.1.xml"/>
|
||||||
|
<include file="META-INF/jpa-changelog-1.7.0.xml"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|
|
@ -12,7 +12,7 @@ public interface JpaUpdaterProvider extends Provider {
|
||||||
|
|
||||||
public String FIRST_VERSION = "1.0.0.Final";
|
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);
|
public String getCurrentVersionSql(String defaultSchema);
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,7 @@ public class IdentityProviderRepresentation {
|
||||||
protected boolean storeToken;
|
protected boolean storeToken;
|
||||||
protected boolean addReadTokenRoleOnCreate;
|
protected boolean addReadTokenRoleOnCreate;
|
||||||
protected boolean authenticateByDefault;
|
protected boolean authenticateByDefault;
|
||||||
|
protected String firstBrokerLoginFlowAlias;
|
||||||
protected Map<String, String> config = new HashMap<String, String>();
|
protected Map<String, String> config = new HashMap<String, String>();
|
||||||
|
|
||||||
public String getInternalId() {
|
public String getInternalId() {
|
||||||
|
@ -127,6 +128,14 @@ public class IdentityProviderRepresentation {
|
||||||
this.authenticateByDefault = authenticateByDefault;
|
this.authenticateByDefault = authenticateByDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFirstBrokerLoginFlowAlias() {
|
||||||
|
return firstBrokerLoginFlowAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFirstBrokerLoginFlowAlias(String firstBrokerLoginFlowAlias) {
|
||||||
|
this.firstBrokerLoginFlowAlias = firstBrokerLoginFlowAlias;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isStoreToken() {
|
public boolean isStoreToken() {
|
||||||
return this.storeToken;
|
return this.storeToken;
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,4 +53,5 @@ public interface Errors {
|
||||||
String EMAIL_SEND_FAILED = "email_send_failed";
|
String EMAIL_SEND_FAILED = "email_send_failed";
|
||||||
String INVALID_EMAIL = "invalid_email";
|
String INVALID_EMAIL = "invalid_email";
|
||||||
String IDENTITY_PROVIDER_LOGIN_FAILURE = "identity_provider_login_failure";
|
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(false),
|
||||||
IDENTITY_PROVIDER_LOGIN_ERROR(false),
|
IDENTITY_PROVIDER_LOGIN_ERROR(false),
|
||||||
|
IDENTITY_PROVIDER_FIRST_LOGIN(true),
|
||||||
|
IDENTITY_PROVIDER_FIRST_LOGIN_ERROR(true),
|
||||||
IDENTITY_PROVIDER_RESPONSE(false),
|
IDENTITY_PROVIDER_RESPONSE(false),
|
||||||
IDENTITY_PROVIDER_RESPONSE_ERROR(false),
|
IDENTITY_PROVIDER_RESPONSE_ERROR(false),
|
||||||
IDENTITY_PROVIDER_RETRIEVE_TOKEN(false),
|
IDENTITY_PROVIDER_RETRIEVE_TOKEN(false),
|
||||||
|
|
|
@ -17,6 +17,7 @@ import javax.security.auth.login.LoginException;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.federation.kerberos.CommonKerberosConfig;
|
import org.keycloak.federation.kerberos.CommonKerberosConfig;
|
||||||
|
import org.keycloak.models.ModelException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -54,6 +55,8 @@ public class KerberosUsernamePasswordAuthenticator {
|
||||||
String message = le.getMessage();
|
String message = le.getMessage();
|
||||||
logger.debug("Message from kerberos: " + message);
|
logger.debug("Message from kerberos: " + message);
|
||||||
|
|
||||||
|
checkKerberosServerAvailable(le);
|
||||||
|
|
||||||
// Bit cumbersome, but seems to work with tested kerberos servers
|
// Bit cumbersome, but seems to work with tested kerberos servers
|
||||||
boolean exists = (!message.contains("Client not found"));
|
boolean exists = (!message.contains("Client not found"));
|
||||||
return exists;
|
return exists;
|
||||||
|
@ -74,11 +77,19 @@ public class KerberosUsernamePasswordAuthenticator {
|
||||||
logoutSubject();
|
logoutSubject();
|
||||||
return true;
|
return true;
|
||||||
} catch (LoginException le) {
|
} catch (LoginException le) {
|
||||||
|
checkKerberosServerAvailable(le);
|
||||||
|
|
||||||
logger.debug("Failed to authenticate user " + username, le);
|
logger.debug("Failed to authenticate user " + username, le);
|
||||||
return false;
|
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
|
* 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...
|
add-provider.placeholder=Add provider...
|
||||||
provider=Provider
|
provider=Provider
|
||||||
gui-order=GUI order
|
gui-order=GUI order
|
||||||
|
first-broker-login-flow=First Login Flow
|
||||||
redirect-uri=Redirect URI
|
redirect-uri=Redirect URI
|
||||||
redirect-uri.tooltip=The redirect uri to use when configuring the identity provider.
|
redirect-uri.tooltip=The redirect uri to use when configuring the identity provider.
|
||||||
alias=Alias
|
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=Trust Email
|
||||||
trust-email.tooltip=If enabled then email provided by this provider is not verified even if verification is enabled for the realm.
|
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).
|
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=OpenID Connect Config
|
||||||
openid-connect-config.tooltip=OIDC SP and external IDP configuration.
|
openid-connect-config.tooltip=OIDC SP and external IDP configuration.
|
||||||
authorization-url=Authorization URL
|
authorization-url=Authorization URL
|
||||||
|
|
|
@ -199,6 +199,9 @@ module.config([ '$routeProvider', function($routeProvider) {
|
||||||
},
|
},
|
||||||
providerFactory : function(IdentityProviderFactoryLoader) {
|
providerFactory : function(IdentityProviderFactoryLoader) {
|
||||||
return {};
|
return {};
|
||||||
|
},
|
||||||
|
authFlows : function(AuthenticationFlowsLoader) {
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
controller : 'RealmIdentityProviderCtrl'
|
controller : 'RealmIdentityProviderCtrl'
|
||||||
|
@ -217,6 +220,9 @@ module.config([ '$routeProvider', function($routeProvider) {
|
||||||
},
|
},
|
||||||
providerFactory : function(IdentityProviderFactoryLoader) {
|
providerFactory : function(IdentityProviderFactoryLoader) {
|
||||||
return new IdentityProviderFactoryLoader();
|
return new IdentityProviderFactoryLoader();
|
||||||
|
},
|
||||||
|
authFlows : function(AuthenticationFlowsLoader) {
|
||||||
|
return AuthenticationFlowsLoader();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
controller : 'RealmIdentityProviderCtrl'
|
controller : 'RealmIdentityProviderCtrl'
|
||||||
|
@ -235,6 +241,9 @@ module.config([ '$routeProvider', function($routeProvider) {
|
||||||
},
|
},
|
||||||
providerFactory : function(IdentityProviderFactoryLoader) {
|
providerFactory : function(IdentityProviderFactoryLoader) {
|
||||||
return IdentityProviderFactoryLoader();
|
return IdentityProviderFactoryLoader();
|
||||||
|
},
|
||||||
|
authFlows : function(AuthenticationFlowsLoader) {
|
||||||
|
return AuthenticationFlowsLoader();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
controller : 'RealmIdentityProviderCtrl'
|
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');
|
console.log('RealmIdentityProviderCtrl');
|
||||||
|
|
||||||
$scope.realm = angular.copy(realm);
|
$scope.realm = angular.copy(realm);
|
||||||
|
@ -678,6 +678,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
|
||||||
$scope.identityProvider.enabled = true;
|
$scope.identityProvider.enabled = true;
|
||||||
$scope.identityProvider.updateProfileFirstLoginMode = "off";
|
$scope.identityProvider.updateProfileFirstLoginMode = "off";
|
||||||
$scope.identityProvider.authenticateByDefault = false;
|
$scope.identityProvider.authenticateByDefault = false;
|
||||||
|
$scope.identityProvider.firstBrokerLoginFlowAlias = 'first broker login';
|
||||||
$scope.newIdentityProvider = true;
|
$scope.newIdentityProvider = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -696,6 +697,13 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
|
||||||
|
|
||||||
$scope.configuredProviders = angular.copy(realm.identityProviders);
|
$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() {
|
$scope.$watch(function() {
|
||||||
return $location.path();
|
return $location.path();
|
||||||
}, function() {
|
}, function() {
|
||||||
|
|
|
@ -79,6 +79,19 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend uncollapsed><span class="text">{{:: 'openid-connect-config' | translate}}</span> <kc-tooltip>{{:: 'openid-connect-config.tooltip' | translate}}</kc-tooltip></legend>
|
<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>
|
</div>
|
||||||
<kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend uncollapsed><span class="text">{{:: 'saml-config' | translate}}</span> <kc-tooltip>{{:: 'identity-provider.saml-config.tooltip' | translate}}</kc-tooltip></legend>
|
<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>
|
</div>
|
||||||
<kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="form-group">
|
<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")}
|
${msg("loginProfileTitle")}
|
||||||
<#elseif section = "form">
|
<#elseif section = "form">
|
||||||
<form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
|
<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.kcFormGroupClass!} ${messagesPerField.printIfExists('username',properties.kcFormGroupErrorClass!)}">
|
||||||
<div class="${properties.kcLabelWrapperClass!}">
|
<div class="${properties.kcLabelWrapperClass!}">
|
||||||
<label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>
|
<label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>
|
||||||
|
|
|
@ -13,7 +13,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="${properties.kcInputWrapperClass!}">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -29,7 +33,7 @@
|
||||||
|
|
||||||
<div class="${properties.kcFormGroupClass!}">
|
<div class="${properties.kcFormGroupClass!}">
|
||||||
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
|
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
|
||||||
<#if realm.rememberMe>
|
<#if realm.rememberMe && !usernameEditDisabled??>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<#if login.rememberMe??>
|
<#if login.rememberMe??>
|
||||||
|
@ -56,7 +60,7 @@
|
||||||
</form>
|
</form>
|
||||||
</#if>
|
</#if>
|
||||||
<#elseif section = "info" >
|
<#elseif section = "info" >
|
||||||
<#if realm.password && realm.registrationAllowed>
|
<#if realm.password && realm.registrationAllowed && !usernameEditDisabled??>
|
||||||
<div id="kc-registration">
|
<div id="kc-registration">
|
||||||
<span>${msg("noAccount")} <a href="${url.registrationUrl}">${msg("doRegister")}</a></span>
|
<span>${msg("noAccount")} <a href="${url.registrationUrl}">${msg("doRegister")}</a></span>
|
||||||
</div>
|
</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?
|
emailVerifyInstruction2=Haven''t received a verification code in your email?
|
||||||
emailVerifyInstruction3=to re-send the 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
|
backToLogin=« Back to Login
|
||||||
|
|
||||||
emailInstruction=Enter your username or email address and we will send you instructions on how to create a new password.
|
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.
|
usernameExistsMessage=Username already exists.
|
||||||
emailExistsMessage=Email already exists.
|
emailExistsMessage=Email already exists.
|
||||||
|
|
||||||
federatedIdentityEmailExistsMessage=User with email 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.
|
||||||
federatedIdentityUsernameExistsMessage=User with username 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.
|
configureTotpMessage=You need to set up Mobile Authenticator to activate your account.
|
||||||
updateProfileMessage=You need to update your user profile 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.
|
updatePasswordMessage=You need to change your password to activate your account.
|
||||||
verifyEmailMessage=You need to verify your email address 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.
|
emailSentMessage=You should receive an email shortly with further instructions.
|
||||||
emailSendErrorMessage=Failed to send email, please try again later.
|
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.
|
unexpectedErrorRetrievingTokenMessage=Unexpected error when retrieving token from identity provider.
|
||||||
unexpectedErrorHandlingResponseMessage=Unexpected error when handling response from identity provider.
|
unexpectedErrorHandlingResponseMessage=Unexpected error when handling response from identity provider.
|
||||||
identityProviderAuthenticationFailedMessage=Authentication failed. Could not authenticate with 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.
|
couldNotSendAuthenticationRequestMessage=Could not send authentication request to identity provider.
|
||||||
unexpectedErrorHandlingRequestMessage=Unexpected error when handling authentication request to identity provider.
|
unexpectedErrorHandlingRequestMessage=Unexpected error when handling authentication request to identity provider.
|
||||||
invalidAccessCodeMessage=Invalid access code.
|
invalidAccessCodeMessage=Invalid access code.
|
||||||
|
@ -188,6 +200,7 @@ sessionNotActiveMessage=Session not active.
|
||||||
invalidCodeMessage=An error occurred, please login again through your application.
|
invalidCodeMessage=An error occurred, please login again through your application.
|
||||||
identityProviderUnexpectedErrorMessage=Unexpected error when authenticating with identity provider
|
identityProviderUnexpectedErrorMessage=Unexpected error when authenticating with identity provider
|
||||||
identityProviderNotFoundMessage=Could not find an identity provider with the identifier.
|
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.
|
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.
|
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.
|
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
|
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.
|
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>
|
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
|
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.
|
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>
|
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 {
|
public interface EmailProvider extends Provider {
|
||||||
|
|
||||||
|
String IDENTITY_PROVIDER_BROKER_CONTEXT = "identityProviderBrokerCtx";
|
||||||
|
|
||||||
public EmailProvider setRealm(RealmModel realm);
|
public EmailProvider setRealm(RealmModel realm);
|
||||||
|
|
||||||
public EmailProvider setUser(UserModel user);
|
public EmailProvider setUser(UserModel user);
|
||||||
|
|
||||||
|
public EmailProvider setAttribute(String name, Object value);
|
||||||
|
|
||||||
public void sendEvent(Event event) throws EmailException;
|
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;
|
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
|
* Change password email requested by admin
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package org.keycloak.email.freemarker;
|
package org.keycloak.email.freemarker;
|
||||||
|
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
@ -17,6 +20,7 @@ import javax.mail.internet.MimeMessage;
|
||||||
import javax.mail.internet.MimeMultipart;
|
import javax.mail.internet.MimeMultipart;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
import org.keycloak.email.EmailException;
|
import org.keycloak.email.EmailException;
|
||||||
import org.keycloak.email.EmailProvider;
|
import org.keycloak.email.EmailProvider;
|
||||||
import org.keycloak.email.freemarker.beans.EventBean;
|
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.Theme;
|
||||||
import org.keycloak.freemarker.ThemeProvider;
|
import org.keycloak.freemarker.ThemeProvider;
|
||||||
import org.keycloak.freemarker.beans.MessageFormatterMethod;
|
import org.keycloak.freemarker.beans.MessageFormatterMethod;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
@ -43,6 +48,7 @@ public class FreeMarkerEmailProvider implements EmailProvider {
|
||||||
private FreeMarkerUtil freeMarker;
|
private FreeMarkerUtil freeMarker;
|
||||||
private RealmModel realm;
|
private RealmModel realm;
|
||||||
private UserModel user;
|
private UserModel user;
|
||||||
|
private final Map<String, Object> attributes = new HashMap<String, Object>();
|
||||||
|
|
||||||
public FreeMarkerEmailProvider(KeycloakSession session, FreeMarkerUtil freeMarker) {
|
public FreeMarkerEmailProvider(KeycloakSession session, FreeMarkerUtil freeMarker) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
|
@ -61,6 +67,12 @@ public class FreeMarkerEmailProvider implements EmailProvider {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EmailProvider setAttribute(String name, Object value) {
|
||||||
|
attributes.put(name, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendEvent(Event event) throws EmailException {
|
public void sendEvent(Event event) throws EmailException {
|
||||||
Map<String, Object> attributes = new HashMap<String, Object>();
|
Map<String, Object> attributes = new HashMap<String, Object>();
|
||||||
|
@ -83,6 +95,27 @@ public class FreeMarkerEmailProvider implements EmailProvider {
|
||||||
send("passwordResetSubject", "password-reset.ftl", attributes);
|
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
|
@Override
|
||||||
public void sendExecuteActions(String link, long expirationInMinutes) throws EmailException {
|
public void sendExecuteActions(String link, long expirationInMinutes) throws EmailException {
|
||||||
Map<String, Object> attributes = new HashMap<String, Object>();
|
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 {
|
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 {
|
try {
|
||||||
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
|
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
|
||||||
Theme theme = themeProvider.getTheme(realm.getEmailTheme(), Theme.Type.EMAIL);
|
Theme theme = themeProvider.getTheme(realm.getEmailTheme(), Theme.Type.EMAIL);
|
||||||
|
@ -118,7 +155,7 @@ public class FreeMarkerEmailProvider implements EmailProvider {
|
||||||
attributes.put("locale", locale);
|
attributes.put("locale", locale);
|
||||||
Properties rb = theme.getMessages(locale);
|
Properties rb = theme.getMessages(locale);
|
||||||
attributes.put("msg", new MessageFormatterMethod(locale, rb));
|
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 textTemplate = String.format("text/%s", template);
|
||||||
String textBody;
|
String textBody;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -5,6 +5,8 @@ package org.keycloak.login;
|
||||||
*/
|
*/
|
||||||
public enum LoginFormsPages {
|
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 {
|
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
|
* Adds a script to the html header
|
||||||
*
|
*
|
||||||
|
@ -44,6 +51,12 @@ public interface LoginFormsProvider extends Provider {
|
||||||
|
|
||||||
public Response createInfoPage();
|
public Response createInfoPage();
|
||||||
|
|
||||||
|
public Response createUpdateProfilePage();
|
||||||
|
|
||||||
|
public Response createIdpLinkConfirmLinkPage();
|
||||||
|
|
||||||
|
public Response createIdpLinkEmailPage();
|
||||||
|
|
||||||
public Response createErrorPage();
|
public Response createErrorPage();
|
||||||
|
|
||||||
public Response createOAuthGrant(ClientSessionModel clientSessionModel);
|
public Response createOAuthGrant(ClientSessionModel clientSessionModel);
|
||||||
|
|
|
@ -19,6 +19,9 @@ package org.keycloak.login.freemarker;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
||||||
import org.keycloak.OAuth2Constants;
|
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.EmailException;
|
||||||
import org.keycloak.email.EmailProvider;
|
import org.keycloak.email.EmailProvider;
|
||||||
import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
|
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.ClientModel;
|
||||||
import org.keycloak.models.ClientSessionModel;
|
import org.keycloak.models.ClientSessionModel;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ProtocolMapperModel;
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
@ -129,6 +133,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
page = LoginFormsPages.LOGIN_CONFIG_TOTP;
|
page = LoginFormsPages.LOGIN_CONFIG_TOTP;
|
||||||
break;
|
break;
|
||||||
case UPDATE_PROFILE:
|
case UPDATE_PROFILE:
|
||||||
|
UpdateProfileContext userBasedContext = new UserUpdateProfileContext(realm, user);
|
||||||
|
this.attributes.put(UPDATE_PROFILE_CONTEXT_ATTR, userBasedContext);
|
||||||
|
|
||||||
actionMessage = Messages.UPDATE_PROFILE;
|
actionMessage = Messages.UPDATE_PROFILE;
|
||||||
page = LoginFormsPages.LOGIN_UPDATE_PROFILE;
|
page = LoginFormsPages.LOGIN_UPDATE_PROFILE;
|
||||||
break;
|
break;
|
||||||
|
@ -140,7 +147,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
try {
|
try {
|
||||||
UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
|
UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
|
||||||
builder.queryParam(OAuth2Constants.CODE, accessCode);
|
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();
|
String link = builder.build(realm.getName()).toString();
|
||||||
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
|
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
|
||||||
|
@ -222,6 +229,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
attributes.put("message", wholeMessage);
|
attributes.put("message", wholeMessage);
|
||||||
|
} else {
|
||||||
|
attributes.put("message", null);
|
||||||
}
|
}
|
||||||
attributes.put("messagesPerField", messagesPerField);
|
attributes.put("messagesPerField", messagesPerField);
|
||||||
|
|
||||||
|
@ -237,7 +246,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
|
|
||||||
if (realm != null) {
|
if (realm != null) {
|
||||||
attributes.put("realm", new RealmBean(realm));
|
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("url", new UrlBean(realm, theme, baseUri, this.actionUri));
|
||||||
|
|
||||||
if (realm.isInternationalizationEnabled()) {
|
if (realm.isInternationalizationEnabled()) {
|
||||||
|
@ -268,7 +281,17 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
attributes.put("totp", new TotpBean(realm, user, baseUri));
|
attributes.put("totp", new TotpBean(realm, user, baseUri));
|
||||||
break;
|
break;
|
||||||
case LOGIN_UPDATE_PROFILE:
|
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;
|
break;
|
||||||
case REGISTER:
|
case REGISTER:
|
||||||
attributes.put("register", new RegisterBean(formData));
|
attributes.put("register", new RegisterBean(formData));
|
||||||
|
@ -371,7 +394,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
|
|
||||||
if (realm != null) {
|
if (realm != null) {
|
||||||
attributes.put("realm", new RealmBean(realm));
|
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("url", new UrlBean(realm, theme, baseUri, this.actionUri));
|
||||||
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
|
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
|
||||||
|
|
||||||
|
@ -423,6 +450,32 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
return createResponse(LoginFormsPages.INFO);
|
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
|
@Override
|
||||||
public Response createErrorPage() {
|
public Response createErrorPage() {
|
||||||
if (status == null) {
|
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";
|
return "login-config-totp.ftl";
|
||||||
case LOGIN_VERIFY_EMAIL:
|
case LOGIN_VERIFY_EMAIL:
|
||||||
return "login-verify-email.ftl";
|
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:
|
case OAUTH_GRANT:
|
||||||
return "login-oauth-grant.ftl";
|
return "login-oauth-grant.ftl";
|
||||||
case LOGIN_RESET_PASSWORD:
|
case LOGIN_RESET_PASSWORD:
|
||||||
|
|
|
@ -44,9 +44,8 @@ public class IdentityProviderBean {
|
||||||
private List<IdentityProvider> providers;
|
private List<IdentityProvider> providers;
|
||||||
private RealmModel realm;
|
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;
|
this.realm = realm;
|
||||||
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
|
|
||||||
|
|
||||||
if (!identityProviders.isEmpty()) {
|
if (!identityProviders.isEmpty()) {
|
||||||
Set<IdentityProvider> orderedSet = new TreeSet<>(IdentityProviderComparator.INSTANCE);
|
Set<IdentityProvider> orderedSet = new TreeSet<>(IdentityProviderComparator.INSTANCE);
|
||||||
|
@ -57,7 +56,7 @@ public class IdentityProviderBean {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!orderedSet.isEmpty()) {
|
if (!orderedSet.isEmpty()) {
|
||||||
providers = new LinkedList<IdentityProvider>(orderedSet);
|
providers = new LinkedList<>(orderedSet);
|
||||||
displaySocial = true;
|
displaySocial = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ import java.util.Map;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
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>
|
* @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 static final Logger logger = Logger.getLogger(ProfileBean.class);
|
||||||
|
|
||||||
private UserModel user;
|
private UpdateProfileContext user;
|
||||||
private MultivaluedMap<String, String> formData;
|
private MultivaluedMap<String, String> formData;
|
||||||
|
|
||||||
private final Map<String, String> attributes = new HashMap<>();
|
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.user = user;
|
||||||
this.formData = formData;
|
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 getUsername() { return formData != null ? formData.getFirst("username") : user.getUsername(); }
|
||||||
|
|
||||||
public String getFirstName() {
|
public String getFirstName() {
|
||||||
|
|
|
@ -90,6 +90,10 @@ public class UrlBean {
|
||||||
return Urls.loginActionEmailVerification(baseURI, realm).toString();
|
return Urls.loginActionEmailVerification(baseURI, realm).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFirstBrokerLoginUrl() {
|
||||||
|
return Urls.firstBrokerLoginProcessor(baseURI, realm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
public String getOauthAction() {
|
public String getOauthAction() {
|
||||||
if (this.actionuri != null) {
|
if (this.actionuri != null) {
|
||||||
return this.actionuri.getPath();
|
return this.actionuri.getPath();
|
||||||
|
|
|
@ -23,5 +23,6 @@ public interface Constants {
|
||||||
// 30 days
|
// 30 days
|
||||||
int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
|
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 boolean authenticateByDefault;
|
||||||
|
|
||||||
|
private String firstBrokerLoginFlowId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>A map containing the configuration and properties for a specific identity provider instance and implementation. The items
|
* <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>
|
* 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.storeToken = model.isStoreToken();
|
||||||
this.authenticateByDefault = model.isAuthenticateByDefault();
|
this.authenticateByDefault = model.isAuthenticateByDefault();
|
||||||
this.addReadTokenRoleOnCreate = model.addReadTokenRoleOnCreate;
|
this.addReadTokenRoleOnCreate = model.addReadTokenRoleOnCreate;
|
||||||
|
this.firstBrokerLoginFlowId = model.getFirstBrokerLoginFlowId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getInternalId() {
|
public String getInternalId() {
|
||||||
|
@ -148,6 +151,14 @@ public class IdentityProviderModel implements Serializable {
|
||||||
this.authenticateByDefault = authenticateByDefault;
|
this.authenticateByDefault = authenticateByDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFirstBrokerLoginFlowId() {
|
||||||
|
return firstBrokerLoginFlowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFirstBrokerLoginFlowId(String firstBrokerLoginFlowId) {
|
||||||
|
this.firstBrokerLoginFlowId = firstBrokerLoginFlowId;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, String> getConfig() {
|
public Map<String, String> getConfig() {
|
||||||
return this.config;
|
return this.config;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ public class IdentityProviderEntity {
|
||||||
private boolean storeToken;
|
private boolean storeToken;
|
||||||
protected boolean addReadTokenRoleOnCreate;
|
protected boolean addReadTokenRoleOnCreate;
|
||||||
private boolean authenticateByDefault;
|
private boolean authenticateByDefault;
|
||||||
|
private String firstBrokerLoginFlowId;
|
||||||
|
|
||||||
private Map<String, String> config = new HashMap<String, String>();
|
private Map<String, String> config = new HashMap<String, String>();
|
||||||
|
|
||||||
|
@ -78,6 +79,14 @@ public class IdentityProviderEntity {
|
||||||
this.authenticateByDefault = authenticateByDefault;
|
this.authenticateByDefault = authenticateByDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFirstBrokerLoginFlowId() {
|
||||||
|
return firstBrokerLoginFlowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFirstBrokerLoginFlowId(String firstBrokerLoginFlowId) {
|
||||||
|
this.firstBrokerLoginFlowId = firstBrokerLoginFlowId;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isStoreToken() {
|
public boolean isStoreToken() {
|
||||||
return this.storeToken;
|
return this.storeToken;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ public class DefaultAuthenticationFlows {
|
||||||
public static final String LOGIN_FORMS_FLOW = "forms";
|
public static final String LOGIN_FORMS_FLOW = "forms";
|
||||||
|
|
||||||
public static final String CLIENT_AUTHENTICATION_FLOW = "clients";
|
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) {
|
public static void addFlows(RealmModel realm) {
|
||||||
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(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(REGISTRATION_FLOW) == null) registrationFlow(realm);
|
||||||
if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
|
if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
|
||||||
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(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) {
|
public static void migrateFlows(RealmModel realm) {
|
||||||
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
|
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(REGISTRATION_FLOW) == null) registrationFlow(realm);
|
||||||
if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
|
if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
|
||||||
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(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) {
|
public static void registrationFlow(RealmModel realm) {
|
||||||
|
@ -309,4 +312,98 @@ public class DefaultAuthenticationFlows {
|
||||||
execution.setAuthenticatorFlow(false);
|
execution.setAuthenticatorFlow(false);
|
||||||
realm.addAuthenticatorExecution(execution);
|
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.bouncycastle.openssl.PEMWriter;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.KeycloakSessionTask;
|
import org.keycloak.models.KeycloakSessionTask;
|
||||||
|
@ -198,9 +199,9 @@ public final class KeycloakModelUtils {
|
||||||
/**
|
/**
|
||||||
* Deep search if given role is descendant of composite role
|
* 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 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"
|
* @return true if "role" is descendant of "composite"
|
||||||
*/
|
*/
|
||||||
public static boolean searchFor(RoleModel role, RoleModel composite, Set<RoleModel> visited) {
|
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
|
* 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
|
* @param username username or email of user
|
||||||
* @return found user
|
* @return found user
|
||||||
*/
|
*/
|
||||||
public static UserModel findUserByNameOrEmail(KeycloakSession session, RealmModel realm, String username) {
|
public static UserModel findUserByNameOrEmail(KeycloakSession session, RealmModel realm, String username) {
|
||||||
UserModel user = session.users().getUserByUsername(username, realm);
|
UserModel user = session.users().getUserByUsername(username, realm);
|
||||||
if (user == null && username.contains("@")) {
|
if (user == null && username.contains("@")) {
|
||||||
user = session.users().getUserByEmail(username, realm);
|
user = session.users().getUserByEmail(username, realm);
|
||||||
}
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
@ -265,7 +266,6 @@ public final class KeycloakModelUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param roles
|
* @param roles
|
||||||
* @param targetRole
|
* @param targetRole
|
||||||
* @return true if targetRole is in roles (directly or indirectly via composite role)
|
* @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.
|
* 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 displayName to check for duplications
|
||||||
* @param myProvider provider, which is excluded from the list (if present)
|
* @param myProvider provider, which is excluded from the list (if present)
|
||||||
* @param federationProviders
|
* @param federationProviders
|
||||||
* @throws ModelDuplicateException if there is other provider with same displayName
|
* @throws ModelDuplicateException if there is other provider with same displayName
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -202,7 +202,7 @@ public class ModelToRepresentation {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (IdentityProviderModel provider : realm.getIdentityProviders()) {
|
for (IdentityProviderModel provider : realm.getIdentityProviders()) {
|
||||||
rep.addIdentityProvider(toRepresentation(provider));
|
rep.addIdentityProvider(toRepresentation(realm, provider));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (IdentityProviderMapperModel mapper : realm.getIdentityProviderMappers()) {
|
for (IdentityProviderMapperModel mapper : realm.getIdentityProviderMappers()) {
|
||||||
|
@ -381,7 +381,7 @@ public class ModelToRepresentation {
|
||||||
return rep;
|
return rep;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IdentityProviderRepresentation toRepresentation(IdentityProviderModel identityProviderModel) {
|
public static IdentityProviderRepresentation toRepresentation(RealmModel realm, IdentityProviderModel identityProviderModel) {
|
||||||
IdentityProviderRepresentation providerRep = new IdentityProviderRepresentation();
|
IdentityProviderRepresentation providerRep = new IdentityProviderRepresentation();
|
||||||
|
|
||||||
providerRep.setInternalId(identityProviderModel.getInternalId());
|
providerRep.setInternalId(identityProviderModel.getInternalId());
|
||||||
|
@ -395,6 +395,15 @@ public class ModelToRepresentation {
|
||||||
providerRep.setConfig(identityProviderModel.getConfig());
|
providerRep.setConfig(identityProviderModel.getConfig());
|
||||||
providerRep.setAddReadTokenRoleOnCreate(identityProviderModel.isAddReadTokenRoleOnCreate());
|
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;
|
return providerRep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -164,6 +164,16 @@ public class RepresentationToModel {
|
||||||
if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep));
|
if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep));
|
||||||
else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
|
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);
|
importIdentityProviders(rep, newRealm);
|
||||||
importIdentityProviderMappers(rep, newRealm);
|
importIdentityProviderMappers(rep, newRealm);
|
||||||
|
|
||||||
|
@ -318,16 +328,6 @@ public class RepresentationToModel {
|
||||||
if(rep.getDefaultLocale() != null){
|
if(rep.getDefaultLocale() != null){
|
||||||
newRealm.setDefaultLocale(rep.getDefaultLocale());
|
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) {
|
public static void importAuthenticationFlows(RealmModel newRealm, RealmRepresentation rep) {
|
||||||
|
@ -1062,7 +1062,7 @@ public class RepresentationToModel {
|
||||||
private static void importIdentityProviders(RealmRepresentation rep, RealmModel newRealm) {
|
private static void importIdentityProviders(RealmRepresentation rep, RealmModel newRealm) {
|
||||||
if (rep.getIdentityProviders() != null) {
|
if (rep.getIdentityProviders() != null) {
|
||||||
for (IdentityProviderRepresentation representation : rep.getIdentityProviders()) {
|
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 identityProviderModel = new IdentityProviderModel();
|
||||||
|
|
||||||
identityProviderModel.setInternalId(representation.getInternalId());
|
identityProviderModel.setInternalId(representation.getInternalId());
|
||||||
|
@ -1087,7 +1087,18 @@ public class RepresentationToModel {
|
||||||
identityProviderModel.setAddReadTokenRoleOnCreate(representation.isAddReadTokenRoleOnCreate());
|
identityProviderModel.setAddReadTokenRoleOnCreate(representation.isAddReadTokenRoleOnCreate());
|
||||||
identityProviderModel.setConfig(representation.getConfig());
|
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) {
|
public static ProtocolMapperModel toModel(ProtocolMapperRepresentation rep) {
|
||||||
|
|
|
@ -1219,6 +1219,7 @@ public class RealmAdapter implements RealmModel {
|
||||||
identityProviderModel.setUpdateProfileFirstLoginMode(entity.getUpdateProfileFirstLoginMode());
|
identityProviderModel.setUpdateProfileFirstLoginMode(entity.getUpdateProfileFirstLoginMode());
|
||||||
identityProviderModel.setTrustEmail(entity.isTrustEmail());
|
identityProviderModel.setTrustEmail(entity.isTrustEmail());
|
||||||
identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault());
|
identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault());
|
||||||
|
identityProviderModel.setFirstBrokerLoginFlowId(entity.getFirstBrokerLoginFlowId());
|
||||||
identityProviderModel.setStoreToken(entity.isStoreToken());
|
identityProviderModel.setStoreToken(entity.isStoreToken());
|
||||||
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());
|
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());
|
||||||
|
|
||||||
|
@ -1252,6 +1253,7 @@ public class RealmAdapter implements RealmModel {
|
||||||
entity.setUpdateProfileFirstLoginMode(identityProvider.getUpdateProfileFirstLoginMode());
|
entity.setUpdateProfileFirstLoginMode(identityProvider.getUpdateProfileFirstLoginMode());
|
||||||
entity.setTrustEmail(identityProvider.isTrustEmail());
|
entity.setTrustEmail(identityProvider.isTrustEmail());
|
||||||
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
||||||
|
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
|
||||||
entity.setConfig(identityProvider.getConfig());
|
entity.setConfig(identityProvider.getConfig());
|
||||||
|
|
||||||
realm.addIdentityProvider(entity);
|
realm.addIdentityProvider(entity);
|
||||||
|
@ -1279,6 +1281,7 @@ public class RealmAdapter implements RealmModel {
|
||||||
entity.setUpdateProfileFirstLoginMode(identityProvider.getUpdateProfileFirstLoginMode());
|
entity.setUpdateProfileFirstLoginMode(identityProvider.getUpdateProfileFirstLoginMode());
|
||||||
entity.setTrustEmail(identityProvider.isTrustEmail());
|
entity.setTrustEmail(identityProvider.isTrustEmail());
|
||||||
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
||||||
|
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
|
||||||
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
|
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
|
||||||
entity.setStoreToken(identityProvider.isStoreToken());
|
entity.setStoreToken(identityProvider.isStoreToken());
|
||||||
entity.setConfig(identityProvider.getConfig());
|
entity.setConfig(identityProvider.getConfig());
|
||||||
|
|
|
@ -11,6 +11,7 @@ import javax.persistence.ManyToOne;
|
||||||
import javax.persistence.MapKeyColumn;
|
import javax.persistence.MapKeyColumn;
|
||||||
import javax.persistence.NamedQueries;
|
import javax.persistence.NamedQueries;
|
||||||
import javax.persistence.NamedQuery;
|
import javax.persistence.NamedQuery;
|
||||||
|
import javax.persistence.OneToOne;
|
||||||
import javax.persistence.Table;
|
import javax.persistence.Table;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -56,6 +57,9 @@ public class IdentityProviderEntity {
|
||||||
@Column(name="AUTHENTICATE_BY_DEFAULT")
|
@Column(name="AUTHENTICATE_BY_DEFAULT")
|
||||||
private boolean authenticateByDefault;
|
private boolean authenticateByDefault;
|
||||||
|
|
||||||
|
@Column(name="FIRST_BROKER_LOGIN_FLOW_ID")
|
||||||
|
private String firstBrokerLoginFlowId;
|
||||||
|
|
||||||
@ElementCollection
|
@ElementCollection
|
||||||
@MapKeyColumn(name="NAME")
|
@MapKeyColumn(name="NAME")
|
||||||
@Column(name="VALUE", columnDefinition = "TEXT")
|
@Column(name="VALUE", columnDefinition = "TEXT")
|
||||||
|
@ -126,6 +130,14 @@ public class IdentityProviderEntity {
|
||||||
this.authenticateByDefault = authenticateByDefault;
|
this.authenticateByDefault = authenticateByDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFirstBrokerLoginFlowId() {
|
||||||
|
return firstBrokerLoginFlowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFirstBrokerLoginFlowId(String firstBrokerLoginFlowId) {
|
||||||
|
this.firstBrokerLoginFlowId = firstBrokerLoginFlowId;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, String> getConfig() {
|
public Map<String, String> getConfig() {
|
||||||
return this.config;
|
return this.config;
|
||||||
}
|
}
|
||||||
|
|
|
@ -826,6 +826,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
||||||
identityProviderModel.setUpdateProfileFirstLoginMode(entity.getUpdateProfileFirstLoginMode());
|
identityProviderModel.setUpdateProfileFirstLoginMode(entity.getUpdateProfileFirstLoginMode());
|
||||||
identityProviderModel.setTrustEmail(entity.isTrustEmail());
|
identityProviderModel.setTrustEmail(entity.isTrustEmail());
|
||||||
identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault());
|
identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault());
|
||||||
|
identityProviderModel.setFirstBrokerLoginFlowId(entity.getFirstBrokerLoginFlowId());
|
||||||
identityProviderModel.setStoreToken(entity.isStoreToken());
|
identityProviderModel.setStoreToken(entity.isStoreToken());
|
||||||
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());
|
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());
|
||||||
|
|
||||||
|
@ -859,6 +860,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
||||||
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
|
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
|
||||||
entity.setStoreToken(identityProvider.isStoreToken());
|
entity.setStoreToken(identityProvider.isStoreToken());
|
||||||
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
||||||
|
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
|
||||||
entity.setConfig(identityProvider.getConfig());
|
entity.setConfig(identityProvider.getConfig());
|
||||||
|
|
||||||
realm.getIdentityProviders().add(entity);
|
realm.getIdentityProviders().add(entity);
|
||||||
|
@ -885,6 +887,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
||||||
entity.setUpdateProfileFirstLoginMode(identityProvider.getUpdateProfileFirstLoginMode());
|
entity.setUpdateProfileFirstLoginMode(identityProvider.getUpdateProfileFirstLoginMode());
|
||||||
entity.setTrustEmail(identityProvider.isTrustEmail());
|
entity.setTrustEmail(identityProvider.isTrustEmail());
|
||||||
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
||||||
|
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
|
||||||
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
|
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
|
||||||
entity.setStoreToken(identityProvider.isStoreToken());
|
entity.setStoreToken(identityProvider.isStoreToken());
|
||||||
entity.setConfig(identityProvider.getConfig());
|
entity.setConfig(identityProvider.getConfig());
|
||||||
|
|
|
@ -32,11 +32,6 @@
|
||||||
<groupId>org.apache.santuario</groupId>
|
<groupId>org.apache.santuario</groupId>
|
||||||
<artifactId>xmlsec</artifactId>
|
<artifactId>xmlsec</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>junit</groupId>
|
|
||||||
<artifactId>junit</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
|
@ -76,6 +76,11 @@ public abstract class AbstractParser implements ParserNamespaceSupport {
|
||||||
* @throws {@link IllegalArgumentException} when the configStream is null
|
* @throws {@link IllegalArgumentException} when the configStream is null
|
||||||
*/
|
*/
|
||||||
public Object parse(InputStream configStream) throws ParsingException {
|
public Object parse(InputStream configStream) throws ParsingException {
|
||||||
|
XMLEventReader xmlEventReader = createEventReader(configStream);
|
||||||
|
return parse(xmlEventReader);
|
||||||
|
}
|
||||||
|
|
||||||
|
public XMLEventReader createEventReader(InputStream configStream) throws ParsingException {
|
||||||
if (configStream == null)
|
if (configStream == null)
|
||||||
throw logger.nullArgumentError("InputStream");
|
throw logger.nullArgumentError("InputStream");
|
||||||
|
|
||||||
|
@ -105,7 +110,7 @@ public abstract class AbstractParser implements ParserNamespaceSupport {
|
||||||
throw logger.parserException(e);
|
throw logger.parserException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parse(xmlEventReader);
|
return xmlEventReader;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClassLoader getTCCL() {
|
private ClassLoader getTCCL() {
|
||||||
|
|
|
@ -136,7 +136,7 @@ public class SAMLAssertionWriter extends BaseWriter {
|
||||||
if (statements != null) {
|
if (statements != null) {
|
||||||
for (StatementAbstractType statement : statements) {
|
for (StatementAbstractType statement : statements) {
|
||||||
if (statement instanceof AuthnStatementType) {
|
if (statement instanceof AuthnStatementType) {
|
||||||
write((AuthnStatementType) statement);
|
write((AuthnStatementType) statement, false);
|
||||||
} else if (statement instanceof AttributeStatementType) {
|
} else if (statement instanceof AttributeStatementType) {
|
||||||
write((AttributeStatementType) statement);
|
write((AttributeStatementType) statement);
|
||||||
} else
|
} else
|
||||||
|
@ -188,8 +188,12 @@ public class SAMLAssertionWriter extends BaseWriter {
|
||||||
*
|
*
|
||||||
* @throws ProcessingException
|
* @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());
|
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();
|
XMLGregorianCalendar authnInstant = authnStatement.getAuthnInstant();
|
||||||
if (authnInstant != null) {
|
if (authnInstant != null) {
|
||||||
|
|
|
@ -22,5 +22,9 @@ public enum AuthenticationFlowError {
|
||||||
CLIENT_NOT_FOUND,
|
CLIENT_NOT_FOUND,
|
||||||
CLIENT_DISABLED,
|
CLIENT_DISABLED,
|
||||||
CLIENT_CREDENTIALS_SETUP_REQUIRED,
|
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;
|
package org.keycloak.authentication.authenticators.resetcred;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
import org.keycloak.authentication.AuthenticationFlowError;
|
import org.keycloak.authentication.AuthenticationFlowError;
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.authentication.AuthenticatorFactory;
|
import org.keycloak.authentication.AuthenticatorFactory;
|
||||||
|
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
||||||
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
||||||
import org.keycloak.email.EmailException;
|
import org.keycloak.email.EmailException;
|
||||||
import org.keycloak.email.EmailProvider;
|
import org.keycloak.email.EmailProvider;
|
||||||
|
@ -36,10 +38,22 @@ import java.util.concurrent.TimeUnit;
|
||||||
*/
|
*/
|
||||||
public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFactory {
|
public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFactory {
|
||||||
|
|
||||||
|
protected static Logger logger = Logger.getLogger(ResetCredentialChooseUser.class);
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "reset-credentials-choose-user";
|
public static final String PROVIDER_ID = "reset-credentials-choose-user";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void authenticate(AuthenticationFlowContext context) {
|
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();
|
Response challenge = context.form().createPasswordReset();
|
||||||
context.challenge(challenge);
|
context.challenge(challenge);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
@ -34,7 +35,7 @@ import java.util.concurrent.TimeUnit;
|
||||||
*/
|
*/
|
||||||
public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory {
|
public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory {
|
||||||
public static final String RESET_CREDENTIAL_SECRET = "RESET_CREDENTIAL_SECRET";
|
public static final String RESET_CREDENTIAL_SECRET = "RESET_CREDENTIAL_SECRET";
|
||||||
public static final String KEY = "key";
|
|
||||||
protected static Logger logger = Logger.getLogger(ResetCredentialEmail.class);
|
protected static Logger logger = Logger.getLogger(ResetCredentialEmail.class);
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "reset-credential-email";
|
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.
|
// it can only be guessed once, and it must match watch is stored in the client session.
|
||||||
String secret = HmacOTP.generateSecret(10);
|
String secret = HmacOTP.generateSecret(10);
|
||||||
context.getClientSession().setNote(RESET_CREDENTIAL_SECRET, secret);
|
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());
|
long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
@ -93,7 +94,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
||||||
@Override
|
@Override
|
||||||
public void action(AuthenticationFlowContext context) {
|
public void action(AuthenticationFlowContext context) {
|
||||||
String secret = context.getClientSession().getNote(RESET_CREDENTIAL_SECRET);
|
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
|
// Can only guess once! We remove the note so another guess can't happen
|
||||||
context.getClientSession().removeNote(RESET_CREDENTIAL_SECRET);
|
context.getClientSession().removeNote(RESET_CREDENTIAL_SECRET);
|
||||||
|
|
|
@ -52,7 +52,7 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
|
||||||
RealmModel realm = context.getRealm();
|
RealmModel realm = context.getRealm();
|
||||||
|
|
||||||
|
|
||||||
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm, formData);
|
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm.isEditUsernameAllowed(), formData);
|
||||||
if (errors != null && !errors.isEmpty()) {
|
if (errors != null && !errors.isEmpty()) {
|
||||||
Response challenge = context.form()
|
Response challenge = context.form()
|
||||||
.setErrors(errors)
|
.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 org.keycloak.services.resources.ThemeResource;
|
||||||
|
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,6 +96,13 @@ public class Urls {
|
||||||
return identityProviderAuthnRequest(baseURI, providerId, realmName, null);
|
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) {
|
public static URI accountTotpPage(URI baseUri, String realmId) {
|
||||||
return accountBase(baseUri).path(AccountService.class, "totpPage").build(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);
|
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){
|
public static String localeCookiePath(URI baseUri, String realmName){
|
||||||
return realmBase(baseUri).path(realmName).build().getRawPath();
|
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 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";
|
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 VERIFY_EMAIL = "verifyEmailMessage";
|
||||||
|
|
||||||
|
public static final String LINK_IDP = "linkIdpMessage";
|
||||||
|
|
||||||
public static final String EMAIL_VERIFIED = "emailVerifiedMessage";
|
public static final String EMAIL_VERIFIED = "emailVerifiedMessage";
|
||||||
|
|
||||||
public static final String EMAIL_SENT = "emailSentMessage";
|
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_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 IDENTITY_PROVIDER_NOT_UNIQUE = "identityProviderNotUniqueMessage";
|
||||||
|
|
||||||
public static final String REALM_SUPPORTS_NO_CREDENTIALS = "realmSupportsNoCredentialsMessage";
|
public static final String REALM_SUPPORTS_NO_CREDENTIALS = "realmSupportsNoCredentialsMessage";
|
||||||
|
|
|
@ -366,7 +366,7 @@ public class AccountService extends AbstractSecuredLocalService {
|
||||||
|
|
||||||
UserModel user = auth.getUser();
|
UserModel user = auth.getUser();
|
||||||
|
|
||||||
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm, formData);
|
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm.isEditUsernameAllowed(), formData);
|
||||||
if (errors != null && !errors.isEmpty()) {
|
if (errors != null && !errors.isEmpty()) {
|
||||||
setReferrerOnPage();
|
setReferrerOnPage();
|
||||||
return account.setErrors(errors).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
|
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.ArrayList;
|
||||||
import java.util.List;
|
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.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
|
@ -21,6 +23,11 @@ public class AttributeFormDataProcessor {
|
||||||
* @param user
|
* @param user
|
||||||
*/
|
*/
|
||||||
public static void process(MultivaluedMap<String, String> formData, RealmModel realm, UserModel 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()) {
|
for (String key : formData.keySet()) {
|
||||||
if (!key.startsWith("user.attributes.")) continue;
|
if (!key.startsWith("user.attributes.")) continue;
|
||||||
String attribute = key.substring("user.attributes.".length());
|
String attribute = key.substring("user.attributes.".length());
|
||||||
|
@ -36,7 +43,6 @@ public class AttributeFormDataProcessor {
|
||||||
|
|
||||||
user.setAttribute(attribute, modelValue);
|
user.setAttribute(attribute, modelValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addOrSetValue(List<String> list, int index, String value) {
|
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.logging.Logger;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
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.common.ClientConnection;
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
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.IdentityProvider;
|
||||||
import org.keycloak.broker.provider.IdentityProviderFactory;
|
import org.keycloak.broker.provider.IdentityProviderFactory;
|
||||||
import org.keycloak.broker.provider.IdentityProviderMapper;
|
import org.keycloak.broker.provider.IdentityProviderMapper;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.AuthenticationFlowModel;
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.ClientSessionModel;
|
import org.keycloak.models.ClientSessionModel;
|
||||||
|
@ -49,7 +54,6 @@ import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.protocol.oidc.TokenManager;
|
import org.keycloak.protocol.oidc.TokenManager;
|
||||||
import org.keycloak.provider.ProviderFactory;
|
import org.keycloak.provider.ProviderFactory;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
|
||||||
import org.keycloak.services.managers.AppAuthManager;
|
import org.keycloak.services.managers.AppAuthManager;
|
||||||
import org.keycloak.services.managers.AuthenticationManager.AuthResult;
|
import org.keycloak.services.managers.AuthenticationManager.AuthResult;
|
||||||
import org.keycloak.services.managers.BruteForceProtector;
|
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.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
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.AccountRoles.MANAGE_ACCOUNT;
|
||||||
import static org.keycloak.models.ClientSessionModel.Action.AUTHENTICATE;
|
import static org.keycloak.models.ClientSessionModel.Action.AUTHENTICATE;
|
||||||
import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
|
import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
|
||||||
import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PROFILE;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p></p>
|
* <p></p>
|
||||||
|
@ -285,27 +289,133 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
}
|
}
|
||||||
|
|
||||||
if (federatedUser == null) {
|
if (federatedUser == null) {
|
||||||
try {
|
|
||||||
federatedUser = createUser(context);
|
|
||||||
|
|
||||||
if (IdentityProviderRepresentation.UPFLM_ON.equals(identityProviderConfig.getUpdateProfileFirstLoginMode())
|
LOGGER.debugf("Federated user not found for provider '%s' and broker username '%s' . Redirecting to flow for firstBrokerLogin", providerId, context.getUsername());
|
||||||
|| (IdentityProviderRepresentation.UPFLM_MISSING.equals(identityProviderConfig.getUpdateProfileFirstLoginMode()) && !Validation.validateUserMandatoryFields(realmModel, federatedUser))) {
|
|
||||||
if (isDebugEnabled()) {
|
String username = context.getModelUsername();
|
||||||
LOGGER.debugf("Identity provider requires update profile action.", federatedUser);
|
if (username == null) {
|
||||||
}
|
if (this.realmModel.isRegistrationEmailAsUsername() && !Validation.isBlank(context.getEmail())) {
|
||||||
federatedUser.addRequiredAction(UPDATE_PROFILE);
|
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 {
|
} else {
|
||||||
updateFederatedIdentity(context, federatedUser);
|
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()
|
UserSessionModel userSession = this.session.sessions()
|
||||||
.createUserSession(this.realmModel, federatedUser, federatedUser.getUsername(), this.clientConnection.getRemoteAddr(), "broker", false, context.getBrokerSessionId(), context.getBrokerUserId());
|
.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);
|
FederatedIdentityModel federatedIdentityModel = this.session.users().getFederatedIdentity(federatedUser, context.getIdpConfig().getAlias(), this.realmModel);
|
||||||
|
|
||||||
// Skip DB write if tokens are null or equal
|
// 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());
|
federatedIdentityModel.setToken(context.getToken());
|
||||||
|
|
||||||
this.session.users().updateFederatedIdentity(this.realmModel, federatedUser, federatedIdentityModel);
|
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());
|
LOGGER.debugf("Got authorization code from client [%s].", client.getClientId());
|
||||||
this.event.client(client);
|
this.event.client(client);
|
||||||
|
this.session.getContext().setClient(client);
|
||||||
|
|
||||||
if (clientSession.getUserSession() != null) {
|
if (clientSession.getUserSession() != null) {
|
||||||
this.event.session(clientSession.getUserSession());
|
this.event.session(clientSession.getUserSession());
|
||||||
|
@ -534,100 +645,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
}
|
}
|
||||||
|
|
||||||
private IdentityProviderModel getIdentityProviderConfig(String providerId) {
|
private IdentityProviderModel getIdentityProviderConfig(String providerId) {
|
||||||
for (IdentityProviderModel model : this.realmModel.getIdentityProviders()) {
|
IdentityProviderModel model = this.realmModel.getIdentityProviderByAlias(providerId);
|
||||||
if (model.getAlias().equals(providerId)) {
|
if (model == null) {
|
||||||
return model;
|
throw new IdentityBrokerException("Configuration for identity provider [" + providerId + "] not found.");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return model;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response corsResponse(Response response, ClientModel clientModel) {
|
private Response corsResponse(Response response, ClientModel clientModel) {
|
||||||
|
|
|
@ -23,8 +23,10 @@ package org.keycloak.services.resources;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
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.requiredactions.VerifyEmail;
|
||||||
|
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||||
|
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
|
@ -33,6 +35,7 @@ import org.keycloak.authentication.RequiredActionContextResult;
|
||||||
import org.keycloak.authentication.RequiredActionFactory;
|
import org.keycloak.authentication.RequiredActionFactory;
|
||||||
import org.keycloak.authentication.RequiredActionProvider;
|
import org.keycloak.authentication.RequiredActionProvider;
|
||||||
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
|
@ -51,7 +54,6 @@ import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserModel.RequiredAction;
|
import org.keycloak.models.UserModel.RequiredAction;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.models.utils.HmacOTP;
|
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.protocol.LoginProtocol;
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.RestartLoginCookie;
|
import org.keycloak.protocol.RestartLoginCookie;
|
||||||
|
@ -92,6 +94,7 @@ public class LoginActionsService {
|
||||||
public static final String REGISTRATION_PATH = "registration";
|
public static final String REGISTRATION_PATH = "registration";
|
||||||
public static final String RESET_CREDENTIALS_PATH = "reset-credentials";
|
public static final String RESET_CREDENTIALS_PATH = "reset-credentials";
|
||||||
public static final String REQUIRED_ACTION = "required-action";
|
public static final String REQUIRED_ACTION = "required-action";
|
||||||
|
public static final String FIRST_BROKER_LOGIN_PATH = "first-broker-login";
|
||||||
|
|
||||||
private RealmModel realm;
|
private RealmModel realm;
|
||||||
|
|
||||||
|
@ -134,6 +137,10 @@ public class LoginActionsService {
|
||||||
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "processRegister");
|
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) {
|
public static UriBuilder loginActionsBaseUrl(UriBuilder baseUriBuilder) {
|
||||||
return baseUriBuilder.path(RealmsResource.class).path(RealmsResource.class, "getLoginActionsService");
|
return baseUriBuilder.path(RealmsResource.class).path(RealmsResource.class, "getLoginActionsService");
|
||||||
}
|
}
|
||||||
|
@ -208,7 +215,7 @@ public class LoginActionsService {
|
||||||
ClientSessionModel clientSession = RestartLoginCookie.restartSession(session, realm, code);
|
ClientSessionModel clientSession = RestartLoginCookie.restartSession(session, realm, code);
|
||||||
if (clientSession != null) {
|
if (clientSession != null) {
|
||||||
event.clone().detail(Details.RESTART_AFTER_TIMEOUT, "true").error(Errors.EXPIRED_CODE);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -267,11 +274,10 @@ public class LoginActionsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Response processAuthentication(String execution, ClientSessionModel clientSession, String errorMessage) {
|
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) {
|
protected Response processFlow(String execution, ClientSessionModel clientSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) {
|
||||||
AuthenticationProcessor processor = new AuthenticationProcessor();
|
|
||||||
processor.setClientSession(clientSession)
|
processor.setClientSession(clientSession)
|
||||||
.setFlowPath(flowPath)
|
.setFlowPath(flowPath)
|
||||||
.setBrowserFlow(true)
|
.setBrowserFlow(true)
|
||||||
|
@ -384,12 +390,33 @@ public class LoginActionsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Response processResetCredentials(String execution, ClientSessionModel clientSession, String errorMessage) {
|
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) {
|
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);
|
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!
|
* OAuth grant page. You should not invoked this directly!
|
||||||
*
|
*
|
||||||
|
@ -627,6 +708,10 @@ public class LoginActionsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getActionCookie() {
|
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);
|
Cookie cookie = headers.getCookies().get(ACTION_COOKIE);
|
||||||
AuthenticationManager.expireCookie(realm, ACTION_COOKIE, AuthenticationManager.getRealmCookiePath(realm, uriInfo), realm.getSslRequired().isRequired(clientConnection), clientConnection);
|
AuthenticationManager.expireCookie(realm, ACTION_COOKIE, AuthenticationManager.getRealmCookiePath(realm, uriInfo), realm.getSslRequired().isRequired(clientConnection), clientConnection);
|
||||||
return cookie != null ? cookie.getValue() : null;
|
return cookie != null ? cookie.getValue() : null;
|
||||||
|
|
|
@ -79,7 +79,7 @@ public class IdentityProviderResource {
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public IdentityProviderRepresentation getIdentityProvider() {
|
public IdentityProviderRepresentation getIdentityProvider() {
|
||||||
this.auth.requireView();
|
this.auth.requireView();
|
||||||
IdentityProviderRepresentation rep = ModelToRepresentation.toRepresentation(this.identityProviderModel);
|
IdentityProviderRepresentation rep = ModelToRepresentation.toRepresentation(realm, this.identityProviderModel);
|
||||||
return rep;
|
return rep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ public class IdentityProviderResource {
|
||||||
String newProviderId = providerRep.getAlias();
|
String newProviderId = providerRep.getAlias();
|
||||||
String oldProviderId = getProviderIdByInternalId(this.realm, internalId);
|
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)) {
|
if (oldProviderId != null && !oldProviderId.equals(newProviderId)) {
|
||||||
|
|
||||||
|
|
|
@ -145,7 +145,7 @@ public class IdentityProvidersResource {
|
||||||
List<IdentityProviderRepresentation> representations = new ArrayList<IdentityProviderRepresentation>();
|
List<IdentityProviderRepresentation> representations = new ArrayList<IdentityProviderRepresentation>();
|
||||||
|
|
||||||
for (IdentityProviderModel identityProviderModel : realm.getIdentityProviders()) {
|
for (IdentityProviderModel identityProviderModel : realm.getIdentityProviders()) {
|
||||||
representations.add(ModelToRepresentation.toRepresentation(identityProviderModel));
|
representations.add(ModelToRepresentation.toRepresentation(realm, identityProviderModel));
|
||||||
}
|
}
|
||||||
return representations;
|
return representations;
|
||||||
}
|
}
|
||||||
|
@ -164,7 +164,7 @@ public class IdentityProvidersResource {
|
||||||
this.auth.requireManage();
|
this.auth.requireManage();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
IdentityProviderModel identityProvider = RepresentationToModel.toModel(representation);
|
IdentityProviderModel identityProvider = RepresentationToModel.toModel(realm, representation);
|
||||||
this.realm.addIdentityProvider(identityProvider);
|
this.realm.addIdentityProvider(identityProvider);
|
||||||
|
|
||||||
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, identityProvider.getInternalId())
|
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, identityProvider.getInternalId())
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package org.keycloak.services.validation;
|
package org.keycloak.services.validation;
|
||||||
|
|
||||||
|
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
|
||||||
import org.keycloak.models.PasswordPolicy;
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
@ -68,13 +68,13 @@ public class Validation {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<FormMessage> validateUpdateProfileForm(MultivaluedMap<String, String> formData) {
|
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<>();
|
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);
|
addError(errors, FIELD_USERNAME, Messages.MISSING_USERNAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ public class Validation {
|
||||||
* @param user to validate
|
* @param user to validate
|
||||||
* @return true if user object contains all mandatory values, false if some mandatory value is missing
|
* @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()));
|
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.ResetCredentialEmail
|
||||||
org.keycloak.authentication.authenticators.resetcred.ResetOTP
|
org.keycloak.authentication.authenticators.resetcred.ResetOTP
|
||||||
org.keycloak.authentication.authenticators.resetcred.ResetPassword
|
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());
|
loginPage.findSocialButton(getProviderId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// TODO: Reenable and adjust to KEYCLOAK-1750 changed behaviour
|
||||||
|
// @Test
|
||||||
public void testUserAlreadyExistsWhenUpdatingProfile() {
|
public void testUserAlreadyExistsWhenUpdatingProfile() {
|
||||||
this.driver.navigate().to("http://localhost:8081/test-app/");
|
this.driver.navigate().to("http://localhost:8081/test-app/");
|
||||||
|
|
||||||
|
@ -469,7 +470,8 @@ public abstract class AbstractIdentityProviderTest {
|
||||||
assertNotNull(federatedUser);
|
assertNotNull(federatedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// TODO: Reenable and adjust to KEYCLOAK-1750 changed behaviour
|
||||||
|
// @Test
|
||||||
public void testUserAlreadyExistsWhenNotUpdatingProfile() {
|
public void testUserAlreadyExistsWhenNotUpdatingProfile() {
|
||||||
IdentityProviderModel identityProviderModel = getIdentityProviderModel();
|
IdentityProviderModel identityProviderModel = getIdentityProviderModel();
|
||||||
|
|
||||||
|
|
|
@ -30,9 +30,9 @@ import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||||
import org.keycloak.broker.saml.SAMLIdentityProvider;
|
import org.keycloak.broker.saml.SAMLIdentityProvider;
|
||||||
import org.keycloak.broker.saml.SAMLIdentityProviderConfig;
|
import org.keycloak.broker.saml.SAMLIdentityProviderConfig;
|
||||||
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
||||||
import org.keycloak.models.ClientModel;
|
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.social.facebook.FacebookIdentityProvider;
|
import org.keycloak.social.facebook.FacebookIdentityProvider;
|
||||||
|
@ -63,7 +63,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
||||||
public void testInstallation() throws Exception {
|
public void testInstallation() throws Exception {
|
||||||
RealmModel realm = installTestRealm();
|
RealmModel realm = installTestRealm();
|
||||||
|
|
||||||
assertIdentityProviderConfig(realm.getIdentityProviders());
|
assertIdentityProviderConfig(realm, realm.getIdentityProviders());
|
||||||
|
|
||||||
assertTrue(realm.isIdentityFederationEnabled());
|
assertTrue(realm.isIdentityFederationEnabled());
|
||||||
this.realmManager.removeRealm(realm);
|
this.realmManager.removeRealm(realm);
|
||||||
|
@ -85,6 +85,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
||||||
identityProviderModel.setTrustEmail(true);
|
identityProviderModel.setTrustEmail(true);
|
||||||
identityProviderModel.setStoreToken(true);
|
identityProviderModel.setStoreToken(true);
|
||||||
identityProviderModel.setAuthenticateByDefault(true);
|
identityProviderModel.setAuthenticateByDefault(true);
|
||||||
|
identityProviderModel.setFirstBrokerLoginFlowId(realm.getBrowserFlow().getId());
|
||||||
|
|
||||||
realm.updateIdentityProvider(identityProviderModel);
|
realm.updateIdentityProvider(identityProviderModel);
|
||||||
|
|
||||||
|
@ -100,6 +101,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
||||||
assertTrue(identityProviderModel.isTrustEmail());
|
assertTrue(identityProviderModel.isTrustEmail());
|
||||||
assertTrue(identityProviderModel.isStoreToken());
|
assertTrue(identityProviderModel.isStoreToken());
|
||||||
assertTrue(identityProviderModel.isAuthenticateByDefault());
|
assertTrue(identityProviderModel.isAuthenticateByDefault());
|
||||||
|
assertEquals(identityProviderModel.getFirstBrokerLoginFlowId(), realm.getBrowserFlow().getId());
|
||||||
|
|
||||||
identityProviderModel.getConfig().remove("config-added");
|
identityProviderModel.getConfig().remove("config-added");
|
||||||
identityProviderModel.setEnabled(true);
|
identityProviderModel.setEnabled(true);
|
||||||
|
@ -122,7 +124,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
||||||
this.realmManager.removeRealm(realm);
|
this.realmManager.removeRealm(realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertIdentityProviderConfig(List<IdentityProviderModel> identityProviders) {
|
private void assertIdentityProviderConfig(RealmModel realm, List<IdentityProviderModel> identityProviders) {
|
||||||
assertFalse(identityProviders.isEmpty());
|
assertFalse(identityProviders.isEmpty());
|
||||||
|
|
||||||
Set<String> checkedProviders = new HashSet<String>(getExpectedProviders());
|
Set<String> checkedProviders = new HashSet<String>(getExpectedProviders());
|
||||||
|
@ -138,9 +140,9 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
||||||
} else if (OIDCIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
} else if (OIDCIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
||||||
assertOidcIdentityProviderConfig(identityProvider);
|
assertOidcIdentityProviderConfig(identityProvider);
|
||||||
} else if (FacebookIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
} else if (FacebookIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
||||||
assertFacebookIdentityProviderConfig(identityProvider);
|
assertFacebookIdentityProviderConfig(realm, identityProvider);
|
||||||
} else if (GitHubIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
} else if (GitHubIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
||||||
assertGitHubIdentityProviderConfig(identityProvider);
|
assertGitHubIdentityProviderConfig(realm, identityProvider);
|
||||||
} else if (TwitterIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
} else if (TwitterIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
||||||
assertTwitterIdentityProviderConfig(identityProvider);
|
assertTwitterIdentityProviderConfig(identityProvider);
|
||||||
} else if (LinkedInIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
} else if (LinkedInIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
|
||||||
|
@ -213,7 +215,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
||||||
assertEquals("clientSecret", config.getClientSecret());
|
assertEquals("clientSecret", config.getClientSecret());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertFacebookIdentityProviderConfig(IdentityProviderModel identityProvider) {
|
private void assertFacebookIdentityProviderConfig(RealmModel realm, IdentityProviderModel identityProvider) {
|
||||||
FacebookIdentityProvider facebookIdentityProvider = new FacebookIdentityProviderFactory().create(identityProvider);
|
FacebookIdentityProvider facebookIdentityProvider = new FacebookIdentityProviderFactory().create(identityProvider);
|
||||||
OAuth2IdentityProviderConfig config = facebookIdentityProvider.getConfig();
|
OAuth2IdentityProviderConfig config = facebookIdentityProvider.getConfig();
|
||||||
|
|
||||||
|
@ -226,12 +228,13 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
||||||
assertEquals(false, config.isStoreToken());
|
assertEquals(false, config.isStoreToken());
|
||||||
assertEquals("clientId", config.getClientId());
|
assertEquals("clientId", config.getClientId());
|
||||||
assertEquals("clientSecret", config.getClientSecret());
|
assertEquals("clientSecret", config.getClientSecret());
|
||||||
|
assertEquals(realm.getBrowserFlow().getId(), identityProvider.getFirstBrokerLoginFlowId());
|
||||||
assertEquals(FacebookIdentityProvider.AUTH_URL, config.getAuthorizationUrl());
|
assertEquals(FacebookIdentityProvider.AUTH_URL, config.getAuthorizationUrl());
|
||||||
assertEquals(FacebookIdentityProvider.TOKEN_URL, config.getTokenUrl());
|
assertEquals(FacebookIdentityProvider.TOKEN_URL, config.getTokenUrl());
|
||||||
assertEquals(FacebookIdentityProvider.PROFILE_URL, config.getUserInfoUrl());
|
assertEquals(FacebookIdentityProvider.PROFILE_URL, config.getUserInfoUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertGitHubIdentityProviderConfig(IdentityProviderModel identityProvider) {
|
private void assertGitHubIdentityProviderConfig(RealmModel realm, IdentityProviderModel identityProvider) {
|
||||||
GitHubIdentityProvider gitHubIdentityProvider = new GitHubIdentityProviderFactory().create(identityProvider);
|
GitHubIdentityProvider gitHubIdentityProvider = new GitHubIdentityProviderFactory().create(identityProvider);
|
||||||
OAuth2IdentityProviderConfig config = gitHubIdentityProvider.getConfig();
|
OAuth2IdentityProviderConfig config = gitHubIdentityProvider.getConfig();
|
||||||
|
|
||||||
|
@ -244,6 +247,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
|
||||||
assertEquals(false, config.isStoreToken());
|
assertEquals(false, config.isStoreToken());
|
||||||
assertEquals("clientId", config.getClientId());
|
assertEquals("clientId", config.getClientId());
|
||||||
assertEquals("clientSecret", config.getClientSecret());
|
assertEquals("clientSecret", config.getClientSecret());
|
||||||
|
assertEquals(realm.getFlowByAlias(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW).getId(), identityProvider.getFirstBrokerLoginFlowId());
|
||||||
assertEquals(GitHubIdentityProvider.AUTH_URL, config.getAuthorizationUrl());
|
assertEquals(GitHubIdentityProvider.AUTH_URL, config.getAuthorizationUrl());
|
||||||
assertEquals(GitHubIdentityProvider.TOKEN_URL, config.getTokenUrl());
|
assertEquals(GitHubIdentityProvider.TOKEN_URL, config.getTokenUrl());
|
||||||
assertEquals(GitHubIdentityProvider.PROFILE_URL, config.getUserInfoUrl());
|
assertEquals(GitHubIdentityProvider.PROFILE_URL, config.getUserInfoUrl());
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"providerId" : "facebook",
|
"providerId" : "facebook",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"updateProfileFirstLogin" : "false",
|
"updateProfileFirstLogin" : "false",
|
||||||
|
"firstBrokerLoginFlowAlias" : "browser",
|
||||||
"config": {
|
"config": {
|
||||||
"authorizationUrl": "authorizationUrl",
|
"authorizationUrl": "authorizationUrl",
|
||||||
"tokenUrl": "tokenUrl",
|
"tokenUrl": "tokenUrl",
|
||||||
|
|
Loading…
Reference in a new issue