KEYCLOAK-1750 Improve first time login with social. Added 'first broker login' flow

This commit is contained in:
mposolda 2015-10-29 11:11:10 +01:00
parent 2b29c3acf4
commit adbf2b22ad
85 changed files with 2573 additions and 196 deletions

View file

@ -92,4 +92,9 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel>
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
}
@Override
public IdentityProviderDataMarshaller getMarshaller() {
return new DefaultDataMarshaller();
}
}

View file

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

View file

@ -103,4 +103,10 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
*/
Response export(UriInfo uriInfo, RealmModel realm, String format);
/**
* Implementation of marshaller to serialize/deserialize attached data to Strings, which can be saved in clientSession
* @return
*/
IdentityProviderDataMarshaller getMarshaller();
}

View file

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

View file

@ -45,6 +45,11 @@
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View file

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

View file

@ -22,6 +22,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.IdentityProviderDataMarshaller;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
@ -263,4 +264,8 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
return SignatureAlgorithm.RSA_SHA256;
}
@Override
public IdentityProviderDataMarshaller getMarshaller() {
return new SAMLDataMarshaller();
}
}

View file

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

View file

@ -13,7 +13,7 @@ public class ObjectUtil {
* @param str2
* @return true if both strings are null or equal
*/
public static boolean isEqualOrNull(Object str1, Object str2) {
public static boolean isEqualOrBothNull(Object str1, Object str2) {
if (str1 == null && str2 == null) {
return true;
}

View file

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

View file

@ -10,4 +10,5 @@
<include file="META-INF/jpa-changelog-1.4.0.xml"/>
<include file="META-INF/jpa-changelog-1.5.0.xml"/>
<include file="META-INF/jpa-changelog-1.6.1.xml"/>
<include file="META-INF/jpa-changelog-1.7.0.xml"/>
</databaseChangeLog>

View file

@ -12,7 +12,7 @@ public interface JpaUpdaterProvider extends Provider {
public String FIRST_VERSION = "1.0.0.Final";
public String LAST_VERSION = "1.6.1";
public String LAST_VERSION = "1.7.0";
public String getCurrentVersionSql(String defaultSchema);

View file

@ -52,6 +52,7 @@ public class IdentityProviderRepresentation {
protected boolean storeToken;
protected boolean addReadTokenRoleOnCreate;
protected boolean authenticateByDefault;
protected String firstBrokerLoginFlowAlias;
protected Map<String, String> config = new HashMap<String, String>();
public String getInternalId() {
@ -127,6 +128,14 @@ public class IdentityProviderRepresentation {
this.authenticateByDefault = authenticateByDefault;
}
public String getFirstBrokerLoginFlowAlias() {
return firstBrokerLoginFlowAlias;
}
public void setFirstBrokerLoginFlowAlias(String firstBrokerLoginFlowAlias) {
this.firstBrokerLoginFlowAlias = firstBrokerLoginFlowAlias;
}
public boolean isStoreToken() {
return this.storeToken;
}

View file

@ -53,4 +53,5 @@ public interface Errors {
String EMAIL_SEND_FAILED = "email_send_failed";
String INVALID_EMAIL = "invalid_email";
String IDENTITY_PROVIDER_LOGIN_FAILURE = "identity_provider_login_failure";
String IDENTITY_PROVIDER_ERROR = "identity_provider_error";
}

View file

@ -60,6 +60,8 @@ public enum EventType {
IDENTITY_PROVIDER_LOGIN(false),
IDENTITY_PROVIDER_LOGIN_ERROR(false),
IDENTITY_PROVIDER_FIRST_LOGIN(true),
IDENTITY_PROVIDER_FIRST_LOGIN_ERROR(true),
IDENTITY_PROVIDER_RESPONSE(false),
IDENTITY_PROVIDER_RESPONSE_ERROR(false),
IDENTITY_PROVIDER_RETRIEVE_TOKEN(false),

View file

@ -17,6 +17,7 @@ import javax.security.auth.login.LoginException;
import org.jboss.logging.Logger;
import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.models.ModelException;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -54,6 +55,8 @@ public class KerberosUsernamePasswordAuthenticator {
String message = le.getMessage();
logger.debug("Message from kerberos: " + message);
checkKerberosServerAvailable(le);
// Bit cumbersome, but seems to work with tested kerberos servers
boolean exists = (!message.contains("Client not found"));
return exists;
@ -74,11 +77,19 @@ public class KerberosUsernamePasswordAuthenticator {
logoutSubject();
return true;
} catch (LoginException le) {
checkKerberosServerAvailable(le);
logger.debug("Failed to authenticate user " + username, le);
return false;
}
}
protected void checkKerberosServerAvailable(LoginException le) {
if (le.getMessage().contains("Port Unreachable")) {
throw new ModelException("Kerberos unreachable", le);
}
}
/**
* Returns true if user was successfully authenticated against Kerberos

View file

@ -374,6 +374,7 @@ table-of-identity-providers=Table of identity providers
add-provider.placeholder=Add provider...
provider=Provider
gui-order=GUI order
first-broker-login-flow=First Login Flow
redirect-uri=Redirect URI
redirect-uri.tooltip=The redirect uri to use when configuring the identity provider.
alias=Alias
@ -393,6 +394,7 @@ update-profile-on-first-login.tooltip=Define conditions under which a user has t
trust-email=Trust Email
trust-email.tooltip=If enabled then email provided by this provider is not verified even if verification is enabled for the realm.
gui-order.tooltip=Number defining order of the provider in GUI (eg. on Login page).
first-broker-login-flow.tooltip=Alias of authentication flow, which is triggered after first login with this identity provider.
openid-connect-config=OpenID Connect Config
openid-connect-config.tooltip=OIDC SP and external IDP configuration.
authorization-url=Authorization URL

View file

@ -199,6 +199,9 @@ module.config([ '$routeProvider', function($routeProvider) {
},
providerFactory : function(IdentityProviderFactoryLoader) {
return {};
},
authFlows : function(AuthenticationFlowsLoader) {
return {};
}
},
controller : 'RealmIdentityProviderCtrl'
@ -217,6 +220,9 @@ module.config([ '$routeProvider', function($routeProvider) {
},
providerFactory : function(IdentityProviderFactoryLoader) {
return new IdentityProviderFactoryLoader();
},
authFlows : function(AuthenticationFlowsLoader) {
return AuthenticationFlowsLoader();
}
},
controller : 'RealmIdentityProviderCtrl'
@ -235,6 +241,9 @@ module.config([ '$routeProvider', function($routeProvider) {
},
providerFactory : function(IdentityProviderFactoryLoader) {
return IdentityProviderFactoryLoader();
},
authFlows : function(AuthenticationFlowsLoader) {
return AuthenticationFlowsLoader();
}
},
controller : 'RealmIdentityProviderCtrl'

View file

@ -594,7 +594,7 @@ module.controller('IdentityProviderTabCtrl', function(Dialog, $scope, Current, N
};
});
module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload, $http, $route, realm, instance, providerFactory, IdentityProvider, serverInfo, $location, Notifications, Dialog) {
module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload, $http, $route, realm, instance, providerFactory, IdentityProvider, serverInfo, authFlows, $location, Notifications, Dialog) {
console.log('RealmIdentityProviderCtrl');
$scope.realm = angular.copy(realm);
@ -678,6 +678,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
$scope.identityProvider.enabled = true;
$scope.identityProvider.updateProfileFirstLoginMode = "off";
$scope.identityProvider.authenticateByDefault = false;
$scope.identityProvider.firstBrokerLoginFlowAlias = 'first broker login';
$scope.newIdentityProvider = true;
}
@ -696,6 +697,13 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
$scope.configuredProviders = angular.copy(realm.identityProviders);
$scope.authFlows = [];
for (var i=0 ; i<authFlows.length ; i++) {
if (authFlows[i].providerId == 'basic-flow') {
$scope.authFlows.push(authFlows[i]);
}
}
$scope.$watch(function() {
return $location.path();
}, function() {

View file

@ -79,6 +79,19 @@
</div>
<kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="firstBrokerLoginFlowAlias">{{:: 'first-broker-login-flow' | translate}}</label>
<div class="col-md-6">
<div>
<select class="form-control" id="firstBrokerLoginFlowAlias"
ng-model="identityProvider.firstBrokerLoginFlowAlias"
ng-options="flow.alias as flow.alias for flow in authFlows"
required>
</select>
</div>
</div>
<kc-tooltip>{{:: 'first-broker-login-flow.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
<fieldset>
<legend uncollapsed><span class="text">{{:: 'openid-connect-config' | translate}}</span> <kc-tooltip>{{:: 'openid-connect-config.tooltip' | translate}}</kc-tooltip></legend>

View file

@ -79,6 +79,19 @@
</div>
<kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="firstBrokerLoginFlowAlias">{{:: 'first-broker-login-flow' | translate}}</label>
<div class="col-md-6">
<div>
<select class="form-control" id="firstBrokerLoginFlowAlias"
ng-model="identityProvider.firstBrokerLoginFlowAlias"
ng-options="flow.alias as flow.alias for flow in authFlows"
required>
</select>
</div>
</div>
<kc-tooltip>{{:: 'first-broker-login-flow.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
<fieldset>
<legend uncollapsed><span class="text">{{:: 'saml-config' | translate}}</span> <kc-tooltip>{{:: 'identity-provider.saml-config.tooltip' | translate}}</kc-tooltip></legend>

View file

@ -97,6 +97,19 @@
</div>
<kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="firstBrokerLoginFlowAlias">{{:: 'first-broker-login-flow' | translate}}</label>
<div class="col-md-6">
<div>
<select class="form-control" id="firstBrokerLoginFlowAlias"
ng-model="identityProvider.firstBrokerLoginFlowAlias"
ng-options="flow.alias as flow.alias for flow in authFlows"
required>
</select>
</div>
</div>
<kc-tooltip>{{:: 'first-broker-login-flow.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
<div class="form-group">

View file

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

View file

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

View file

@ -6,7 +6,7 @@
${msg("loginProfileTitle")}
<#elseif section = "form">
<form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<#if realm.editUsernameAllowed>
<#if user.editUsernameAllowed>
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('username',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>

View file

@ -13,7 +13,11 @@
</div>
<div class="${properties.kcInputWrapperClass!}">
<input id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')?html}" type="text" autofocus />
<#if usernameEditDisabled??>
<input id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')?html}" type="text" disabled />
<#else>
<input id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')?html}" type="text" autofocus />
</#if>
</div>
</div>
@ -29,7 +33,7 @@
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
<#if realm.rememberMe>
<#if realm.rememberMe && !usernameEditDisabled??>
<div class="checkbox">
<label>
<#if login.rememberMe??>
@ -56,7 +60,7 @@
</form>
</#if>
<#elseif section = "info" >
<#if realm.password && realm.registrationAllowed>
<#if realm.password && realm.registrationAllowed && !usernameEditDisabled??>
<div id="kc-registration">
<span>${msg("noAccount")} <a href="${url.registrationUrl}">${msg("doRegister")}</a></span>
</div>

View file

@ -79,6 +79,11 @@ emailVerifyInstruction1=An email with instructions to verify your email address
emailVerifyInstruction2=Haven''t received a verification code in your email?
emailVerifyInstruction3=to re-send the email.
emailLinkIdpTitle=Link {0}
emailLinkIdp1=An email with instructions to link {0} account {1} with your {2} account has been sent to you.
emailLinkIdp2=Haven''t received a verification code in your email?
emailLinkIdp3=to re-send the email.
backToLogin=&laquo; Back to Login
emailInstruction=Enter your username or email address and we will send you instructions on how to create a new password.
@ -132,13 +137,19 @@ invalidTotpMessage=Invalid authenticator code.
usernameExistsMessage=Username already exists.
emailExistsMessage=Email already exists.
federatedIdentityEmailExistsMessage=User with email already exists. Please login to account management to link the account.
federatedIdentityUsernameExistsMessage=User with username already exists. Please login to account management to link the account.
federatedIdentityExistsMessage=User with {0} {1} already exists. Please login to account management to link the account.
confirmLinkIdpTitle=Account already exists
federatedIdentityConfirmLinkMessage=User with {0} {1} already exists. How do you want to continue?
federatedIdentityConfirmReauthenticateMessage=Authenticate as {0} to link your account with {1}
confirmLinkIdpUpdateProfile=Update profile info
confirmLinkIdpContinue=Link {0} with existing account
configureTotpMessage=You need to set up Mobile Authenticator to activate your account.
updateProfileMessage=You need to update your user profile to activate your account.
updatePasswordMessage=You need to change your password to activate your account.
verifyEmailMessage=You need to verify your email address to activate your account.
linkIdpMessage=You need to verify your email address to link your account with {0}.
emailSentMessage=You should receive an email shortly with further instructions.
emailSendErrorMessage=Failed to send email, please try again later.
@ -181,6 +192,7 @@ couldNotObtainTokenMessage=Could not obtain token from identity provider.
unexpectedErrorRetrievingTokenMessage=Unexpected error when retrieving token from identity provider.
unexpectedErrorHandlingResponseMessage=Unexpected error when handling response from identity provider.
identityProviderAuthenticationFailedMessage=Authentication failed. Could not authenticate with identity provider.
identityProviderDifferentUserMessage=Authenticated as {0}, but expected to be authenticated as {1}
couldNotSendAuthenticationRequestMessage=Could not send authentication request to identity provider.
unexpectedErrorHandlingRequestMessage=Unexpected error when handling authentication request to identity provider.
invalidAccessCodeMessage=Invalid access code.
@ -188,6 +200,7 @@ sessionNotActiveMessage=Session not active.
invalidCodeMessage=An error occurred, please login again through your application.
identityProviderUnexpectedErrorMessage=Unexpected error when authenticating with identity provider
identityProviderNotFoundMessage=Could not find an identity provider with the identifier.
identityProviderLinkSuccess=Your account was successfully linked with {0} account {1} .
realmSupportsNoCredentialsMessage=Realm does not support any credential type.
identityProviderNotUniqueMessage=Realm supports multiple identity providers. Could not determine which identity provider should be used to authenticate with.
emailVerifiedMessage=Your email address has been verified.

View file

@ -0,0 +1,5 @@
<html>
<body>
${msg("identityProviderLinkBodyHtml", identityProviderAlias, realmName, identityProviderContext.username, link, linkExpiration)}
</body>
</html>

View file

@ -1,6 +1,9 @@
emailVerificationSubject=Verify email
emailVerificationBody=Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you didn''t create this account, just ignore this message.
emailVerificationBodyHtml=<p>Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you didn''t create this account, just ignore this message.</p>
identityProviderLinkSubject=Link {0}
identityProviderLinkBody=Someone wants to link your "{1}" account with "{0}" account of user {2} . If this was you, click the link below to link accounts\n\n{3}\n\nThis link will expire within {4} minutes.\n\nIf you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.
identityProviderLinkBodyHtml=<p>Someone wants to link your <b>{1}</b> account with <b>{0}</b> account of user {2} . If this was you, click the link below to link accounts</p><p><a href="{3}">{3}</a></p><p>This link will expire within {4} minutes.</p><p>If you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.</p>
passwordResetSubject=Reset password
passwordResetBody=Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.\n\n{0}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your credentials, just ignore this message and nothing will be changed.
passwordResetBodyHtml=<p>Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your credentials, just ignore this message and nothing will be changed.</p>

View file

@ -0,0 +1 @@
${msg("identityProviderLinkBody", identityProviderAlias, realmName, identityProviderContext.username, link, linkExpiration)}

View file

@ -10,10 +10,14 @@ import org.keycloak.provider.Provider;
*/
public interface EmailProvider extends Provider {
String IDENTITY_PROVIDER_BROKER_CONTEXT = "identityProviderBrokerCtx";
public EmailProvider setRealm(RealmModel realm);
public EmailProvider setUser(UserModel user);
public EmailProvider setAttribute(String name, Object value);
public void sendEvent(Event event) throws EmailException;
/**
@ -25,6 +29,11 @@ public interface EmailProvider extends Provider {
*/
public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException;
/**
* Send to confirm that user wants to link his account with identity broker link
*/
void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException;
/**
* Change password email requested by admin
*

View file

@ -1,8 +1,11 @@
package org.keycloak.email.freemarker;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
@ -17,6 +20,7 @@ import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import org.jboss.logging.Logger;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider;
import org.keycloak.email.freemarker.beans.EventBean;
@ -28,6 +32,7 @@ import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.freemarker.beans.MessageFormatterMethod;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@ -43,6 +48,7 @@ public class FreeMarkerEmailProvider implements EmailProvider {
private FreeMarkerUtil freeMarker;
private RealmModel realm;
private UserModel user;
private final Map<String, Object> attributes = new HashMap<String, Object>();
public FreeMarkerEmailProvider(KeycloakSession session, FreeMarkerUtil freeMarker) {
this.session = session;
@ -61,6 +67,12 @@ public class FreeMarkerEmailProvider implements EmailProvider {
return this;
}
@Override
public EmailProvider setAttribute(String name, Object value) {
attributes.put(name, value);
return this;
}
@Override
public void sendEvent(Event event) throws EmailException {
Map<String, Object> attributes = new HashMap<String, Object>();
@ -83,6 +95,27 @@ public class FreeMarkerEmailProvider implements EmailProvider {
send("passwordResetSubject", "password-reset.ftl", attributes);
}
@Override
public void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException {
Map<String, Object> attributes = new HashMap<String, Object>();
attributes.put("user", new ProfileBean(user));
attributes.put("link", link);
attributes.put("linkExpiration", expirationInMinutes);
String realmName = realm.getName().substring(0, 1).toUpperCase() + realm.getName().substring(1);
attributes.put("realmName", realmName);
BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT);
String idpAlias = brokerContext.getIdpConfig().getAlias();
idpAlias = idpAlias.substring(0, 1).toUpperCase() + idpAlias.substring(1);
attributes.put("identityProviderContext", brokerContext);
attributes.put("identityProviderAlias", idpAlias);
List<Object> subjectAttrs = Arrays.<Object>asList(idpAlias);
send("identityProviderLinkSubject", subjectAttrs, "identity-provider-link.ftl", attributes);
}
@Override
public void sendExecuteActions(String link, long expirationInMinutes) throws EmailException {
Map<String, Object> attributes = new HashMap<String, Object>();
@ -111,6 +144,10 @@ public class FreeMarkerEmailProvider implements EmailProvider {
}
private void send(String subjectKey, String template, Map<String, Object> attributes) throws EmailException {
send(subjectKey, Collections.emptyList(), template, attributes);
}
private void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
try {
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
Theme theme = themeProvider.getTheme(realm.getEmailTheme(), Theme.Type.EMAIL);
@ -118,7 +155,7 @@ public class FreeMarkerEmailProvider implements EmailProvider {
attributes.put("locale", locale);
Properties rb = theme.getMessages(locale);
attributes.put("msg", new MessageFormatterMethod(locale, rb));
String subject = new MessageFormat(rb.getProperty(subjectKey,subjectKey),locale).format(new Object[0]);
String subject = new MessageFormat(rb.getProperty(subjectKey,subjectKey),locale).format(subjectAttributes.toArray());
String textTemplate = String.format("text/%s", template);
String textBody;
try {

View file

@ -5,6 +5,8 @@ package org.keycloak.login;
*/
public enum LoginFormsPages {
LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL, OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, INFO, ERROR, LOGIN_UPDATE_PROFILE, CODE;
LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL,
LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL,
OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, INFO, ERROR, LOGIN_UPDATE_PROFILE, CODE;
}

View file

@ -23,6 +23,13 @@ import org.keycloak.provider.Provider;
*/
public interface LoginFormsProvider extends Provider {
String UPDATE_PROFILE_CONTEXT_ATTR = "updateProfileCtx";
String IDENTITY_PROVIDER_BROKER_CONTEXT = "identityProviderBrokerCtx";
String USERNAME_EDIT_DISABLED = "usernameEditDisabled";
/**
* Adds a script to the html header
*
@ -44,6 +51,12 @@ public interface LoginFormsProvider extends Provider {
public Response createInfoPage();
public Response createUpdateProfilePage();
public Response createIdpLinkConfirmLinkPage();
public Response createIdpLinkEmailPage();
public Response createErrorPage();
public Response createOAuthGrant(ClientSessionModel clientSessionModel);

View file

@ -19,6 +19,9 @@ package org.keycloak.login.freemarker;
import org.jboss.logging.Logger;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
import org.keycloak.authentication.requiredactions.util.UserUpdateProfileContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider;
import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
@ -48,6 +51,7 @@ import org.keycloak.login.freemarker.model.UrlBean;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
@ -129,6 +133,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
page = LoginFormsPages.LOGIN_CONFIG_TOTP;
break;
case UPDATE_PROFILE:
UpdateProfileContext userBasedContext = new UserUpdateProfileContext(realm, user);
this.attributes.put(UPDATE_PROFILE_CONTEXT_ATTR, userBasedContext);
actionMessage = Messages.UPDATE_PROFILE;
page = LoginFormsPages.LOGIN_UPDATE_PROFILE;
break;
@ -140,7 +147,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
try {
UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
builder.queryParam(OAuth2Constants.CODE, accessCode);
builder.queryParam("key", clientSession.getNote(Constants.VERIFY_EMAIL_KEY));
builder.queryParam(Constants.KEY, clientSession.getNote(Constants.VERIFY_EMAIL_KEY));
String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
@ -222,6 +229,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
}
attributes.put("message", wholeMessage);
} else {
attributes.put("message", null);
}
attributes.put("messagesPerField", messagesPerField);
@ -237,7 +246,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
if (realm != null) {
attributes.put("realm", new RealmBean(realm));
attributes.put("social", new IdentityProviderBean(realm, baseUri, uriInfo));
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
attributes.put("social", new IdentityProviderBean(realm, identityProviders, baseUri, uriInfo));
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
if (realm.isInternationalizationEnabled()) {
@ -268,7 +281,17 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
attributes.put("totp", new TotpBean(realm, user, baseUri));
break;
case LOGIN_UPDATE_PROFILE:
attributes.put("user", new ProfileBean(user, formData));
UpdateProfileContext userCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
attributes.put("user", new ProfileBean(userCtx, formData));
break;
case LOGIN_IDP_LINK_CONFIRM:
case LOGIN_IDP_LINK_EMAIL:
BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT);
String idpAlias = brokerContext.getIdpConfig().getAlias();
idpAlias = idpAlias.substring(0, 1).toUpperCase() + idpAlias.substring(1);
attributes.put("brokerContext", brokerContext);
attributes.put("idpAlias", idpAlias);
break;
case REGISTER:
attributes.put("register", new RegisterBean(formData));
@ -371,7 +394,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
if (realm != null) {
attributes.put("realm", new RealmBean(realm));
attributes.put("social", new IdentityProviderBean(realm, baseUri, uriInfo));
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
attributes.put("social", new IdentityProviderBean(realm, identityProviders, baseUri, uriInfo));
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
@ -423,6 +450,32 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
return createResponse(LoginFormsPages.INFO);
}
@Override
public Response createUpdateProfilePage() {
// Don't display initial message if we already have some errors
if (messageType != MessageType.ERROR) {
setMessage(MessageType.WARNING, Messages.UPDATE_PROFILE);
}
return createResponse(LoginFormsPages.LOGIN_UPDATE_PROFILE);
}
@Override
public Response createIdpLinkConfirmLinkPage() {
return createResponse(LoginFormsPages.LOGIN_IDP_LINK_CONFIRM);
}
@Override
public Response createIdpLinkEmailPage() {
BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT);
String idpAlias = brokerContext.getIdpConfig().getAlias();
idpAlias = idpAlias.substring(0, 1).toUpperCase() + idpAlias.substring(1);
setMessage(MessageType.WARNING, Messages.LINK_IDP, idpAlias);
return createResponse(LoginFormsPages.LOGIN_IDP_LINK_EMAIL);
}
@Override
public Response createErrorPage() {
if (status == null) {

View file

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

View file

@ -17,6 +17,10 @@ public class Templates {
return "login-config-totp.ftl";
case LOGIN_VERIFY_EMAIL:
return "login-verify-email.ftl";
case LOGIN_IDP_LINK_CONFIRM:
return "login-idp-link-confirm.ftl";
case LOGIN_IDP_LINK_EMAIL:
return "login-idp-link-email.ftl";
case OAUTH_GRANT:
return "login-oauth-grant.ftl";
case LOGIN_RESET_PASSWORD:

View file

@ -44,9 +44,8 @@ public class IdentityProviderBean {
private List<IdentityProvider> providers;
private RealmModel realm;
public IdentityProviderBean(RealmModel realm, URI baseURI, UriInfo uriInfo) {
public IdentityProviderBean(RealmModel realm, List<IdentityProviderModel> identityProviders, URI baseURI, UriInfo uriInfo) {
this.realm = realm;
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
if (!identityProviders.isEmpty()) {
Set<IdentityProvider> orderedSet = new TreeSet<>(IdentityProviderComparator.INSTANCE);
@ -57,7 +56,7 @@ public class IdentityProviderBean {
}
if (!orderedSet.isEmpty()) {
providers = new LinkedList<IdentityProvider>(orderedSet);
providers = new LinkedList<>(orderedSet);
displaySocial = true;
}
}

View file

@ -28,7 +28,7 @@ import java.util.Map;
import javax.ws.rs.core.MultivaluedMap;
import org.jboss.logging.Logger;
import org.keycloak.models.UserModel;
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -38,12 +38,12 @@ public class ProfileBean {
private static final Logger logger = Logger.getLogger(ProfileBean.class);
private UserModel user;
private UpdateProfileContext user;
private MultivaluedMap<String, String> formData;
private final Map<String, String> attributes = new HashMap<>();
public ProfileBean(UserModel user, MultivaluedMap<String, String> formData) {
public ProfileBean(UpdateProfileContext user, MultivaluedMap<String, String> formData) {
this.user = user;
this.formData = formData;
@ -70,6 +70,10 @@ public class ProfileBean {
}
public boolean isEditUsernameAllowed() {
return user.isEditUsernameAllowed();
}
public String getUsername() { return formData != null ? formData.getFirst("username") : user.getUsername(); }
public String getFirstName() {

View file

@ -90,6 +90,10 @@ public class UrlBean {
return Urls.loginActionEmailVerification(baseURI, realm).toString();
}
public String getFirstBrokerLoginUrl() {
return Urls.firstBrokerLoginProcessor(baseURI, realm).toString();
}
public String getOauthAction() {
if (this.actionuri != null) {
return this.actionuri.getPath();

View file

@ -23,5 +23,6 @@ public interface Constants {
// 30 days
int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
public static final String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
String KEY = "key";
}

View file

@ -64,6 +64,8 @@ public class IdentityProviderModel implements Serializable {
*/
private boolean authenticateByDefault;
private String firstBrokerLoginFlowId;
/**
* <p>A map containing the configuration and properties for a specific identity provider instance and implementation. The items
* in the map are understood by the identity provider implementation.</p>
@ -84,6 +86,7 @@ public class IdentityProviderModel implements Serializable {
this.storeToken = model.isStoreToken();
this.authenticateByDefault = model.isAuthenticateByDefault();
this.addReadTokenRoleOnCreate = model.addReadTokenRoleOnCreate;
this.firstBrokerLoginFlowId = model.getFirstBrokerLoginFlowId();
}
public String getInternalId() {
@ -148,6 +151,14 @@ public class IdentityProviderModel implements Serializable {
this.authenticateByDefault = authenticateByDefault;
}
public String getFirstBrokerLoginFlowId() {
return firstBrokerLoginFlowId;
}
public void setFirstBrokerLoginFlowId(String firstBrokerLoginFlowId) {
this.firstBrokerLoginFlowId = firstBrokerLoginFlowId;
}
public Map<String, String> getConfig() {
return this.config;
}

View file

@ -35,6 +35,7 @@ public class IdentityProviderEntity {
private boolean storeToken;
protected boolean addReadTokenRoleOnCreate;
private boolean authenticateByDefault;
private String firstBrokerLoginFlowId;
private Map<String, String> config = new HashMap<String, String>();
@ -78,6 +79,14 @@ public class IdentityProviderEntity {
this.authenticateByDefault = authenticateByDefault;
}
public String getFirstBrokerLoginFlowId() {
return firstBrokerLoginFlowId;
}
public void setFirstBrokerLoginFlowId(String firstBrokerLoginFlowId) {
this.firstBrokerLoginFlowId = firstBrokerLoginFlowId;
}
public boolean isStoreToken() {
return this.storeToken;
}

View file

@ -19,6 +19,7 @@ public class DefaultAuthenticationFlows {
public static final String LOGIN_FORMS_FLOW = "forms";
public static final String CLIENT_AUTHENTICATION_FLOW = "clients";
public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login";
public static void addFlows(RealmModel realm) {
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm);
@ -26,6 +27,7 @@ public class DefaultAuthenticationFlows {
if (realm.getFlowByAlias(REGISTRATION_FLOW) == null) registrationFlow(realm);
if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm);
}
public static void migrateFlows(RealmModel realm) {
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
@ -33,6 +35,7 @@ public class DefaultAuthenticationFlows {
if (realm.getFlowByAlias(REGISTRATION_FLOW) == null) registrationFlow(realm);
if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm);
}
public static void registrationFlow(RealmModel realm) {
@ -309,4 +312,98 @@ public class DefaultAuthenticationFlows {
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
}
public static void firstBrokerLoginFlow(RealmModel realm) {
AuthenticationFlowModel firstBrokerLogin = new AuthenticationFlowModel();
firstBrokerLogin.setAlias(FIRST_BROKER_LOGIN_FLOW);
firstBrokerLogin.setDescription("Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account");
firstBrokerLogin.setProviderId("basic-flow");
firstBrokerLogin.setTopLevel(true);
firstBrokerLogin.setBuiltIn(true);
firstBrokerLogin = realm.addAuthenticationFlow(firstBrokerLogin);
// realm.setClientAuthenticationFlow(clients);
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
execution.setParentFlow(firstBrokerLogin.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("idp-update-profile");
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(firstBrokerLogin.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
execution.setAuthenticator("idp-detect-duplications");
execution.setPriority(20);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
AuthenticationFlowModel linkExistingAccountFlow = new AuthenticationFlowModel();
linkExistingAccountFlow.setTopLevel(false);
linkExistingAccountFlow.setBuiltIn(true);
linkExistingAccountFlow.setAlias("Handle Existing Account");
linkExistingAccountFlow.setDescription("Handle what to do if there is existing account with same email/username like authenticated identity provider");
linkExistingAccountFlow.setProviderId("basic-flow");
linkExistingAccountFlow = realm.addAuthenticationFlow(linkExistingAccountFlow);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(firstBrokerLogin.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
execution.setFlowId(linkExistingAccountFlow.getId());
execution.setPriority(30);
execution.setAuthenticatorFlow(true);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(linkExistingAccountFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("idp-confirm-link");
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(linkExistingAccountFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
execution.setAuthenticator("idp-email-verification");
execution.setPriority(20);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
AuthenticationFlowModel verifyByReauthenticationAccountFlow = new AuthenticationFlowModel();
verifyByReauthenticationAccountFlow.setTopLevel(false);
verifyByReauthenticationAccountFlow.setBuiltIn(true);
verifyByReauthenticationAccountFlow.setAlias("Verify Existing Account by Re-authentication");
verifyByReauthenticationAccountFlow.setDescription("Reauthentication of existing account");
verifyByReauthenticationAccountFlow.setProviderId("basic-flow");
verifyByReauthenticationAccountFlow = realm.addAuthenticationFlow(verifyByReauthenticationAccountFlow);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(linkExistingAccountFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
execution.setFlowId(verifyByReauthenticationAccountFlow.getId());
execution.setPriority(30);
execution.setAuthenticatorFlow(true);
realm.addAuthenticatorExecution(execution);
// password + otp
execution = new AuthenticationExecutionModel();
execution.setParentFlow(verifyByReauthenticationAccountFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("idp-username-password-form");
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(verifyByReauthenticationAccountFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL);
// TODO: read the requirement from browser authenticator
// if (migrate && hasCredentialType(realm, RequiredCredentialModel.TOTP.getType())) {
// execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
// }
execution.setAuthenticator("auth-otp-form");
execution.setPriority(20);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
}
}

View file

@ -3,6 +3,7 @@ package org.keycloak.models.utils;
import org.bouncycastle.openssl.PEMWriter;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
@ -198,9 +199,9 @@ public final class KeycloakModelUtils {
/**
* Deep search if given role is descendant of composite role
*
* @param role role to check
* @param role role to check
* @param composite composite role
* @param visited set of already visited roles (used for recursion)
* @param visited set of already visited roles (used for recursion)
* @return true if "role" is descendant of "composite"
*/
public static boolean searchFor(RoleModel role, RoleModel composite, Set<RoleModel> visited) {
@ -218,14 +219,14 @@ public final class KeycloakModelUtils {
/**
* Try to find user by given username. If it fails, then fallback to find him by email
*
* @param realm realm
* @param realm realm
* @param username username or email of user
* @return found user
*/
public static UserModel findUserByNameOrEmail(KeycloakSession session, RealmModel realm, String username) {
UserModel user = session.users().getUserByUsername(username, realm);
if (user == null && username.contains("@")) {
user = session.users().getUserByEmail(username, realm);
user = session.users().getUserByEmail(username, realm);
}
return user;
}
@ -265,7 +266,6 @@ public final class KeycloakModelUtils {
}
/**
*
* @param roles
* @param targetRole
* @return true if targetRole is in roles (directly or indirectly via composite role)
@ -284,8 +284,8 @@ public final class KeycloakModelUtils {
/**
* Ensure that displayName of myProvider (if not null) is unique and there is no other provider with same displayName in the list.
*
* @param displayName to check for duplications
* @param myProvider provider, which is excluded from the list (if present)
* @param displayName to check for duplications
* @param myProvider provider, which is excluded from the list (if present)
* @param federationProviders
* @throws ModelDuplicateException if there is other provider with same displayName
*/

View file

@ -202,7 +202,7 @@ public class ModelToRepresentation {
}
for (IdentityProviderModel provider : realm.getIdentityProviders()) {
rep.addIdentityProvider(toRepresentation(provider));
rep.addIdentityProvider(toRepresentation(realm, provider));
}
for (IdentityProviderMapperModel mapper : realm.getIdentityProviderMappers()) {
@ -381,7 +381,7 @@ public class ModelToRepresentation {
return rep;
}
public static IdentityProviderRepresentation toRepresentation(IdentityProviderModel identityProviderModel) {
public static IdentityProviderRepresentation toRepresentation(RealmModel realm, IdentityProviderModel identityProviderModel) {
IdentityProviderRepresentation providerRep = new IdentityProviderRepresentation();
providerRep.setInternalId(identityProviderModel.getInternalId());
@ -395,6 +395,15 @@ public class ModelToRepresentation {
providerRep.setConfig(identityProviderModel.getConfig());
providerRep.setAddReadTokenRoleOnCreate(identityProviderModel.isAddReadTokenRoleOnCreate());
String firstBrokerLoginFlowId = identityProviderModel.getFirstBrokerLoginFlowId();
if (firstBrokerLoginFlowId != null) {
AuthenticationFlowModel flow = realm.getAuthenticationFlowById(firstBrokerLoginFlowId);
if (flow == null) {
throw new ModelException("Couldn't find authentication flow with id " + firstBrokerLoginFlowId);
}
providerRep.setFirstBrokerLoginFlowAlias(flow.getAlias());
}
return providerRep;
}

View file

@ -164,6 +164,16 @@ public class RepresentationToModel {
if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep));
else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
importAuthenticationFlows(newRealm, rep);
if (rep.getRequiredActions() != null) {
for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) {
RequiredActionProviderModel model = toModel(action);
newRealm.addRequiredActionProvider(model);
}
} else {
DefaultRequiredActions.addActions(newRealm);
}
importIdentityProviders(rep, newRealm);
importIdentityProviderMappers(rep, newRealm);
@ -318,16 +328,6 @@ public class RepresentationToModel {
if(rep.getDefaultLocale() != null){
newRealm.setDefaultLocale(rep.getDefaultLocale());
}
importAuthenticationFlows(newRealm, rep);
if (rep.getRequiredActions() != null) {
for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) {
RequiredActionProviderModel model = toModel(action);
newRealm.addRequiredActionProvider(model);
}
} else {
DefaultRequiredActions.addActions(newRealm);
}
}
public static void importAuthenticationFlows(RealmModel newRealm, RealmRepresentation rep) {
@ -1062,7 +1062,7 @@ public class RepresentationToModel {
private static void importIdentityProviders(RealmRepresentation rep, RealmModel newRealm) {
if (rep.getIdentityProviders() != null) {
for (IdentityProviderRepresentation representation : rep.getIdentityProviders()) {
newRealm.addIdentityProvider(toModel(representation));
newRealm.addIdentityProvider(toModel(newRealm, representation));
}
}
}
@ -1073,7 +1073,7 @@ public class RepresentationToModel {
}
}
}
public static IdentityProviderModel toModel(IdentityProviderRepresentation representation) {
public static IdentityProviderModel toModel(RealmModel realm, IdentityProviderRepresentation representation) {
IdentityProviderModel identityProviderModel = new IdentityProviderModel();
identityProviderModel.setInternalId(representation.getInternalId());
@ -1087,7 +1087,18 @@ public class RepresentationToModel {
identityProviderModel.setAddReadTokenRoleOnCreate(representation.isAddReadTokenRoleOnCreate());
identityProviderModel.setConfig(representation.getConfig());
return identityProviderModel;
String flowAlias = representation.getFirstBrokerLoginFlowAlias();
if (flowAlias == null) {
flowAlias = DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW;
}
AuthenticationFlowModel flowModel = realm.getFlowByAlias(flowAlias);
if (flowModel == null) {
throw new ModelException("No available authentication flow with alias: " + flowAlias);
}
identityProviderModel.setFirstBrokerLoginFlowId(flowModel.getId());
return identityProviderModel;
}
public static ProtocolMapperModel toModel(ProtocolMapperRepresentation rep) {

View file

@ -1219,6 +1219,7 @@ public class RealmAdapter implements RealmModel {
identityProviderModel.setUpdateProfileFirstLoginMode(entity.getUpdateProfileFirstLoginMode());
identityProviderModel.setTrustEmail(entity.isTrustEmail());
identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault());
identityProviderModel.setFirstBrokerLoginFlowId(entity.getFirstBrokerLoginFlowId());
identityProviderModel.setStoreToken(entity.isStoreToken());
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());
@ -1252,6 +1253,7 @@ public class RealmAdapter implements RealmModel {
entity.setUpdateProfileFirstLoginMode(identityProvider.getUpdateProfileFirstLoginMode());
entity.setTrustEmail(identityProvider.isTrustEmail());
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
entity.setConfig(identityProvider.getConfig());
realm.addIdentityProvider(entity);
@ -1279,6 +1281,7 @@ public class RealmAdapter implements RealmModel {
entity.setUpdateProfileFirstLoginMode(identityProvider.getUpdateProfileFirstLoginMode());
entity.setTrustEmail(identityProvider.isTrustEmail());
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
entity.setStoreToken(identityProvider.isStoreToken());
entity.setConfig(identityProvider.getConfig());

View file

@ -11,6 +11,7 @@ import javax.persistence.ManyToOne;
import javax.persistence.MapKeyColumn;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import java.util.Map;
@ -56,6 +57,9 @@ public class IdentityProviderEntity {
@Column(name="AUTHENTICATE_BY_DEFAULT")
private boolean authenticateByDefault;
@Column(name="FIRST_BROKER_LOGIN_FLOW_ID")
private String firstBrokerLoginFlowId;
@ElementCollection
@MapKeyColumn(name="NAME")
@Column(name="VALUE", columnDefinition = "TEXT")
@ -126,6 +130,14 @@ public class IdentityProviderEntity {
this.authenticateByDefault = authenticateByDefault;
}
public String getFirstBrokerLoginFlowId() {
return firstBrokerLoginFlowId;
}
public void setFirstBrokerLoginFlowId(String firstBrokerLoginFlowId) {
this.firstBrokerLoginFlowId = firstBrokerLoginFlowId;
}
public Map<String, String> getConfig() {
return this.config;
}

View file

@ -826,6 +826,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
identityProviderModel.setUpdateProfileFirstLoginMode(entity.getUpdateProfileFirstLoginMode());
identityProviderModel.setTrustEmail(entity.isTrustEmail());
identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault());
identityProviderModel.setFirstBrokerLoginFlowId(entity.getFirstBrokerLoginFlowId());
identityProviderModel.setStoreToken(entity.isStoreToken());
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());
@ -859,6 +860,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
entity.setStoreToken(identityProvider.isStoreToken());
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
entity.setConfig(identityProvider.getConfig());
realm.getIdentityProviders().add(entity);
@ -885,6 +887,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
entity.setUpdateProfileFirstLoginMode(identityProvider.getUpdateProfileFirstLoginMode());
entity.setTrustEmail(identityProvider.isTrustEmail());
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
entity.setStoreToken(identityProvider.isStoreToken());
entity.setConfig(identityProvider.getConfig());

View file

@ -32,11 +32,6 @@
<groupId>org.apache.santuario</groupId>
<artifactId>xmlsec</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>

View file

@ -76,6 +76,11 @@ public abstract class AbstractParser implements ParserNamespaceSupport {
* @throws {@link IllegalArgumentException} when the configStream is null
*/
public Object parse(InputStream configStream) throws ParsingException {
XMLEventReader xmlEventReader = createEventReader(configStream);
return parse(xmlEventReader);
}
public XMLEventReader createEventReader(InputStream configStream) throws ParsingException {
if (configStream == null)
throw logger.nullArgumentError("InputStream");
@ -105,7 +110,7 @@ public abstract class AbstractParser implements ParserNamespaceSupport {
throw logger.parserException(e);
}
return parse(xmlEventReader);
return xmlEventReader;
}
private ClassLoader getTCCL() {

View file

@ -136,7 +136,7 @@ public class SAMLAssertionWriter extends BaseWriter {
if (statements != null) {
for (StatementAbstractType statement : statements) {
if (statement instanceof AuthnStatementType) {
write((AuthnStatementType) statement);
write((AuthnStatementType) statement, false);
} else if (statement instanceof AttributeStatementType) {
write((AttributeStatementType) statement);
} else
@ -188,8 +188,12 @@ public class SAMLAssertionWriter extends BaseWriter {
*
* @throws ProcessingException
*/
public void write(AuthnStatementType authnStatement) throws ProcessingException {
public void write(AuthnStatementType authnStatement, boolean includeNamespace) throws ProcessingException {
StaxUtil.writeStartElement(writer, ASSERTION_PREFIX, JBossSAMLConstants.AUTHN_STATEMENT.get(), ASSERTION_NSURI.get());
if (includeNamespace) {
StaxUtil.writeNameSpace(writer, ASSERTION_PREFIX, ASSERTION_NSURI.get());
StaxUtil.writeDefaultNameSpace(writer, ASSERTION_NSURI.get());
}
XMLGregorianCalendar authnInstant = authnStatement.getAuthnInstant();
if (authnInstant != null) {

View file

@ -22,5 +22,9 @@ public enum AuthenticationFlowError {
CLIENT_NOT_FOUND,
CLIENT_DISABLED,
CLIENT_CREDENTIALS_SETUP_REQUIRED,
INVALID_CLIENT_CREDENTIALS
INVALID_CLIENT_CREDENTIALS,
IDENTITY_PROVIDER_NOT_FOUND,
IDENTITY_PROVIDER_DISABLED,
IDENTITY_PROVIDER_ERROR
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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";
}
}

View file

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

View file

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

View file

@ -1,10 +1,12 @@
package org.keycloak.authentication.authenticators.resetcred;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider;
@ -36,10 +38,22 @@ import java.util.concurrent.TimeUnit;
*/
public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFactory {
protected static Logger logger = Logger.getLogger(ResetCredentialChooseUser.class);
public static final String PROVIDER_ID = "reset-credentials-choose-user";
@Override
public void authenticate(AuthenticationFlowContext context) {
String existingUserId = context.getClientSession().getNote(AbstractIdpAuthenticator.EXISTING_USER_INFO);
if (existingUserId != null) {
UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getClientSession());
logger.debugf("Forget-password triggered when reauthenticating user after first broker login. Skipping reset-credential-choose-user screen and using user '%s' ", existingUser.getUsername());
context.setUser(existingUser);
context.success();
return;
}
Response challenge = context.form().createPasswordReset();
context.challenge(challenge);
}

View file

@ -14,6 +14,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
@ -34,7 +35,7 @@ import java.util.concurrent.TimeUnit;
*/
public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory {
public static final String RESET_CREDENTIAL_SECRET = "RESET_CREDENTIAL_SECRET";
public static final String KEY = "key";
protected static Logger logger = Logger.getLogger(ResetCredentialEmail.class);
public static final String PROVIDER_ID = "reset-credential-email";
@ -67,7 +68,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
// it can only be guessed once, and it must match watch is stored in the client session.
String secret = HmacOTP.generateSecret(10);
context.getClientSession().setNote(RESET_CREDENTIAL_SECRET, secret);
String link = UriBuilder.fromUri(context.getActionUrl()).queryParam(KEY, secret).build().toString();
String link = UriBuilder.fromUri(context.getActionUrl()).queryParam(Constants.KEY, secret).build().toString();
long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
try {
@ -93,7 +94,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
@Override
public void action(AuthenticationFlowContext context) {
String secret = context.getClientSession().getNote(RESET_CREDENTIAL_SECRET);
String key = context.getUriInfo().getQueryParameters().getFirst(KEY);
String key = context.getUriInfo().getQueryParameters().getFirst(Constants.KEY);
// Can only guess once! We remove the note so another guess can't happen
context.getClientSession().removeNote(RESET_CREDENTIAL_SECRET);

View file

@ -52,7 +52,7 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
RealmModel realm = context.getRealm();
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm, formData);
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm.isEditUsernameAllowed(), formData);
if (errors != null && !errors.isEmpty()) {
Response challenge = context.form()
.setErrors(errors)

View file

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

View file

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

View file

@ -32,6 +32,8 @@ import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.resources.ThemeResource;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
/**
@ -94,6 +96,13 @@ public class Urls {
return identityProviderAuthnRequest(baseURI, providerId, realmName, null);
}
public static URI identityProviderAfterFirstBrokerLogin(URI baseUri, String realmName, String accessCode) {
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
.path(IdentityBrokerService.class, "afterFirstBrokerLogin")
.replaceQueryParam(OAuth2Constants.CODE, accessCode)
.build(realmName);
}
public static URI accountTotpPage(URI baseUri, String realmId) {
return accountBase(baseUri).path(AccountService.class, "totpPage").build(realmId);
}
@ -204,6 +213,11 @@ public class Urls {
return loginActionsBase(baseUri).path(LoginActionsService.class, "processConsent").build(realmId);
}
public static URI firstBrokerLoginProcessor(URI baseUri, String realmName) {
return loginActionsBase(baseUri).path(LoginActionsService.class, "firstBrokerLoginGet")
.build(realmName);
}
public static String localeCookiePath(URI baseUri, String realmName){
return realmBase(baseUri).path(realmName).build().getRawPath();
}

View file

@ -64,9 +64,13 @@ public class Messages {
public static final String EMAIL_EXISTS = "emailExistsMessage";
public static final String FEDERATED_IDENTITY_EMAIL_EXISTS = "federatedIdentityEmailExistsMessage";
public static final String FEDERATED_IDENTITY_EXISTS = "federatedIdentityExistsMessage";
public static final String FEDERATED_IDENTITY_USERNAME_EXISTS = "federatedIdentityUsernameExistsMessage";
public static final String FEDERATED_IDENTITY_CONFIRM_LINK_MESSAGE = "federatedIdentityConfirmLinkMessage";
public static final String FEDERATED_IDENTITY_CONFIRM_REAUTHENTICATE_MESSAGE = "federatedIdentityConfirmReauthenticateMessage";
public static final String IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE = "identityProviderDifferentUserMessage";
public static final String CONFIGURE_TOTP = "configureTotpMessage";
@ -76,6 +80,8 @@ public class Messages {
public static final String VERIFY_EMAIL = "verifyEmailMessage";
public static final String LINK_IDP = "linkIdpMessage";
public static final String EMAIL_VERIFIED = "emailVerifiedMessage";
public static final String EMAIL_SENT = "emailSentMessage";
@ -147,6 +153,8 @@ public class Messages {
public static final String IDENTITY_PROVIDER_NOT_FOUND = "identityProviderNotFoundMessage";
public static final String IDENTITY_PROVIDER_LINK_SUCCESS = "identityProviderLinkSuccess";
public static final String IDENTITY_PROVIDER_NOT_UNIQUE = "identityProviderNotUniqueMessage";
public static final String REALM_SUPPORTS_NO_CREDENTIALS = "realmSupportsNoCredentialsMessage";

View file

@ -366,7 +366,7 @@ public class AccountService extends AbstractSecuredLocalService {
UserModel user = auth.getUser();
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm, formData);
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm.isEditUsernameAllowed(), formData);
if (errors != null && !errors.isEmpty()) {
setReferrerOnPage();
return account.setErrors(errors).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);

View file

@ -3,6 +3,8 @@ package org.keycloak.services.resources;
import java.util.ArrayList;
import java.util.List;
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
import org.keycloak.authentication.requiredactions.util.UserUpdateProfileContext;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@ -21,6 +23,11 @@ public class AttributeFormDataProcessor {
* @param user
*/
public static void process(MultivaluedMap<String, String> formData, RealmModel realm, UserModel user) {
UpdateProfileContext userCtx = new UserUpdateProfileContext(realm, user);
process(formData, realm, userCtx);
}
public static void process(MultivaluedMap<String, String> formData, RealmModel realm, UpdateProfileContext user) {
for (String key : formData.keySet()) {
if (!key.startsWith("user.attributes.")) continue;
String attribute = key.substring("user.attributes.".length());
@ -36,7 +43,6 @@ public class AttributeFormDataProcessor {
user.setAttribute(attribute, modelValue);
}
}
private static void addOrSetValue(List<String> list, int index, String value) {

View file

@ -20,6 +20,9 @@ package org.keycloak.services.resources;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.common.ClientConnection;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.broker.provider.AuthenticationRequest;
@ -28,10 +31,12 @@ import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.IdentityProviderFactory;
import org.keycloak.broker.provider.IdentityProviderMapper;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
@ -49,7 +54,6 @@ import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager.AuthResult;
import org.keycloak.services.managers.BruteForceProtector;
@ -70,6 +74,7 @@ import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -79,7 +84,6 @@ import java.util.Set;
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
import static org.keycloak.models.ClientSessionModel.Action.AUTHENTICATE;
import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PROFILE;
/**
* <p></p>
@ -285,27 +289,133 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
if (federatedUser == null) {
try {
federatedUser = createUser(context);
if (IdentityProviderRepresentation.UPFLM_ON.equals(identityProviderConfig.getUpdateProfileFirstLoginMode())
|| (IdentityProviderRepresentation.UPFLM_MISSING.equals(identityProviderConfig.getUpdateProfileFirstLoginMode()) && !Validation.validateUserMandatoryFields(realmModel, federatedUser))) {
if (isDebugEnabled()) {
LOGGER.debugf("Identity provider requires update profile action.", federatedUser);
}
federatedUser.addRequiredAction(UPDATE_PROFILE);
LOGGER.debugf("Federated user not found for provider '%s' and broker username '%s' . Redirecting to flow for firstBrokerLogin", providerId, context.getUsername());
String username = context.getModelUsername();
if (username == null) {
if (this.realmModel.isRegistrationEmailAsUsername() && !Validation.isBlank(context.getEmail())) {
username = context.getEmail();
} else if (context.getUsername() == null) {
username = context.getIdpConfig().getAlias() + "." + context.getId();
} else {
username = context.getIdpConfig().getAlias() + "." + context.getUsername();
}
if(identityProviderConfig.isTrustEmail() && !Validation.isBlank(federatedUser.getEmail())){
federatedUser.setEmailVerified(true);
}
} catch (Exception e) {
return redirectToLoginPage(e, clientCode);
}
username = username.trim();
context.setModelUsername(username);
clientSession.setTimestamp(Time.currentTime());
SerializedBrokeredIdentityContext ctx = SerializedBrokeredIdentityContext.serialize(context);
ctx.saveToClientSession(clientSession);
URI redirect = LoginActionsService.firstBrokerLoginProcessor(uriInfo)
.queryParam(OAuth2Constants.CODE, context.getCode())
.build(realmModel.getName());
return Response.status(302).location(redirect).build();
} else {
updateFederatedIdentity(context, federatedUser);
boolean firstBrokerLoginInProgress = (clientSession.getNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null);
if (firstBrokerLoginInProgress) {
LOGGER.debugf("Reauthenticated with broker '%s' when linking user '%s' with other broker", context.getIdpConfig().getAlias(), federatedUser.getUsername());
UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realmModel, clientSession);
if (!linkingUser.getId().equals(federatedUser.getId())) {
return redirectToErrorPage(Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE, federatedUser.getUsername(), linkingUser.getUsername());
}
clientSession.setAuthenticatedUser(federatedUser);
return afterFirstBrokerLogin(context.getCode());
}
return finishBrokerAuthentication(context, federatedUser, clientSession, providerId);
}
}
// Callback from LoginActionsService after first login with broker was done and Keycloak account is successfully linked/created
@GET
@Path("/after-first-broker-login")
public Response afterFirstBrokerLogin(@QueryParam("code") String code) {
ClientSessionCode clientCode = parseClientSessionCode(code);
ClientSessionModel clientSession = clientCode.getClientSession();
try {
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession);
if (serializedCtx == null) {
throw new IdentityBrokerException("Not found serialized context in clientSession");
}
BrokeredIdentityContext context = serializedCtx.deserialize(session, clientSession);
String providerId = context.getIdpConfig().getAlias();
// firstBrokerLogin workflow finished. Removing note now
clientSession.removeNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
UserModel federatedUser = clientSession.getAuthenticatedUser();
if (federatedUser == null) {
throw new IdentityBrokerException("Couldn't found authenticated federatedUser in clientSession");
}
if (context.getIdpConfig().isAddReadTokenRoleOnCreate()) {
RoleModel readTokenRole = realmModel.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE);
federatedUser.grantRole(readTokenRole);
}
// Add federated identity link here
FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(context.getIdpConfig().getAlias(), context.getId(),
context.getUsername(), context.getToken());
session.users().addFederatedIdentity(realmModel, federatedUser, federatedIdentityModel);
String isRegisteredNewUser = clientSession.getNote(AbstractIdpAuthenticator.BROKER_REGISTERED_NEW_USER);
if (Boolean.parseBoolean(isRegisteredNewUser)) {
LOGGER.debugf("Registered new user '%s' after first login with identity provider '%s'. Identity provider username is '%s' . ", federatedUser.getUsername(), providerId, context.getUsername());
context.getIdp().importNewUser(session, realmModel, federatedUser, context);
Set<IdentityProviderMapperModel> mappers = realmModel.getIdentityProviderMappersByAlias(providerId);
if (mappers != null) {
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
for (IdentityProviderMapperModel mapper : mappers) {
IdentityProviderMapper target = (IdentityProviderMapper)sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
target.importNewUser(session, realmModel, federatedUser, mapper, context);
}
}
if (context.getIdpConfig().isTrustEmail() && !Validation.isBlank(federatedUser.getEmail()) && !Boolean.parseBoolean(clientSession.getNote(AbstractIdpAuthenticator.UPDATE_PROFILE_EMAIL_CHANGED))) {
LOGGER.debugf("Email verified automatically after registration of user '%s' through Identity provider '%s' ", federatedUser.getUsername(), context.getIdpConfig().getAlias());
federatedUser.setEmailVerified(true);
}
this.event.clone().user(federatedUser).event(EventType.REGISTER)
.detail(Details.IDENTITY_PROVIDER, providerId)
.detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername())
.removeDetail("auth_method")
.success();
} else {
LOGGER.debugf("Linked existing keycloak user '%s' with identity provider '%s' . Identity provider username is '%s' .", federatedUser.getUsername(), providerId, context.getUsername());
updateFederatedIdentity(context, federatedUser);
}
String isDifferentBrowser = clientSession.getNote(AbstractIdpAuthenticator.IS_DIFFERENT_BROWSER);
if (Boolean.parseBoolean(isDifferentBrowser)) {
session.sessions().removeClientSession(realmModel, clientSession);
return session.getProvider(LoginFormsProvider.class)
.setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, context.getIdpConfig().getAlias(), context.getUsername())
.createInfoPage();
} else {
return finishBrokerAuthentication(context, federatedUser, clientSession, providerId);
}
} catch (Exception e) {
// TODO?
return redirectToErrorPage(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, e);
}
}
private Response finishBrokerAuthentication(BrokeredIdentityContext context, UserModel federatedUser, ClientSessionModel clientSession, String providerId) {
UserSessionModel userSession = this.session.sessions()
.createUserSession(this.realmModel, federatedUser, federatedUser.getUsername(), this.clientConnection.getRemoteAddr(), "broker", false, context.getBrokerSessionId(), context.getBrokerUserId());
@ -376,7 +486,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
FederatedIdentityModel federatedIdentityModel = this.session.users().getFederatedIdentity(federatedUser, context.getIdpConfig().getAlias(), this.realmModel);
// Skip DB write if tokens are null or equal
if (context.getIdpConfig().isStoreToken() && !ObjectUtil.isEqualOrNull(context.getToken(), federatedIdentityModel.getToken())) {
if (context.getIdpConfig().isStoreToken() && !ObjectUtil.isEqualOrBothNull(context.getToken(), federatedIdentityModel.getToken())) {
federatedIdentityModel.setToken(context.getToken());
this.session.users().updateFederatedIdentity(this.realmModel, federatedUser, federatedIdentityModel);
@ -412,6 +522,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
LOGGER.debugf("Got authorization code from client [%s].", client.getClientId());
this.event.client(client);
this.session.getContext().setClient(client);
if (clientSession.getUserSession() != null) {
this.event.session(clientSession.getUserSession());
@ -534,100 +645,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
private IdentityProviderModel getIdentityProviderConfig(String providerId) {
for (IdentityProviderModel model : this.realmModel.getIdentityProviders()) {
if (model.getAlias().equals(providerId)) {
return model;
}
IdentityProviderModel model = this.realmModel.getIdentityProviderByAlias(providerId);
if (model == null) {
throw new IdentityBrokerException("Configuration for identity provider [" + providerId + "] not found.");
}
throw new IdentityBrokerException("Configuration for identity provider [" + providerId + "] not found.");
}
private UserModel createUser(BrokeredIdentityContext context) {
FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(context.getIdpConfig().getAlias(), context.getId(),
context.getUsername(), context.getToken());
// Check if no user already exists with this username or email
UserModel existingUser = null;
if (context.getEmail() != null) {
existingUser = this.session.users().getUserByEmail(context.getEmail(), this.realmModel);
}
if (existingUser != null) {
fireErrorEvent(Errors.FEDERATED_IDENTITY_EMAIL_EXISTS);
throw new IdentityBrokerException(Messages.FEDERATED_IDENTITY_EMAIL_EXISTS);
}
String username = context.getModelUsername();
if (username == null) {
username = context.getUsername();
if (this.realmModel.isRegistrationEmailAsUsername() && !Validation.isBlank(context.getEmail())) {
username = context.getEmail();
} else if (username == null) {
username = context.getIdpConfig().getAlias() + "." + context.getId();
} else {
username = context.getIdpConfig().getAlias() + "." + context.getUsername();
}
}
if (username == null) {
LOGGER.warn("Unknown username");
fireErrorEvent(Errors.FEDERATED_IDENTITY_USERNAME_EXISTS);
throw new IdentityBrokerException(Messages.FEDERATED_IDENTITY_USERNAME_EXISTS);
}
username = username.trim();
existingUser = this.session.users().getUserByUsername(username, this.realmModel);
if (existingUser != null) {
fireErrorEvent(Errors.FEDERATED_IDENTITY_USERNAME_EXISTS);
throw new IdentityBrokerException(Messages.FEDERATED_IDENTITY_USERNAME_EXISTS);
}
if (isDebugEnabled()) {
LOGGER.debugf("Creating account from identity [%s].", federatedIdentityModel);
}
UserModel federatedUser = this.session.users().addUser(this.realmModel, username);
if (isDebugEnabled()) {
LOGGER.debugf("Account [%s] created.", federatedUser);
}
federatedUser.setEnabled(true);
federatedUser.setEmail(context.getEmail());
federatedUser.setFirstName(context.getFirstName());
federatedUser.setLastName(context.getLastName());
if (context.getIdpConfig().isAddReadTokenRoleOnCreate()) {
RoleModel readTokenRole = realmModel.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE);
federatedUser.grantRole(readTokenRole);
}
if (context.getIdpConfig().isStoreToken()) {
federatedIdentityModel.setToken(context.getToken());
}
this.session.users().addFederatedIdentity(this.realmModel, federatedUser, federatedIdentityModel);
context.getIdp().importNewUser(session, realmModel, federatedUser, context);
Set<IdentityProviderMapperModel> mappers = realmModel.getIdentityProviderMappersByAlias(context.getIdpConfig().getAlias());
if (mappers != null) {
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
for (IdentityProviderMapperModel mapper : mappers) {
IdentityProviderMapper target = (IdentityProviderMapper)sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
target.importNewUser(session, realmModel, federatedUser, mapper, context);
}
}
this.event.clone().user(federatedUser).event(EventType.REGISTER)
.detail(Details.IDENTITY_PROVIDER, federatedIdentityModel.getIdentityProvider())
.detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername())
.removeDetail("auth_method")
.success();
return federatedUser;
return model;
}
private Response corsResponse(Response response, ClientModel clientModel) {

View file

@ -23,8 +23,10 @@ package org.keycloak.services.resources;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.requiredactions.VerifyEmail;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.common.ClientConnection;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationProcessor;
@ -33,6 +35,7 @@ import org.keycloak.authentication.RequiredActionContextResult;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
@ -51,7 +54,6 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.HmacOTP;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.RestartLoginCookie;
@ -92,6 +94,7 @@ public class LoginActionsService {
public static final String REGISTRATION_PATH = "registration";
public static final String RESET_CREDENTIALS_PATH = "reset-credentials";
public static final String REQUIRED_ACTION = "required-action";
public static final String FIRST_BROKER_LOGIN_PATH = "first-broker-login";
private RealmModel realm;
@ -134,6 +137,10 @@ public class LoginActionsService {
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "processRegister");
}
public static UriBuilder firstBrokerLoginProcessor(UriInfo uriInfo) {
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "firstBrokerLoginGet");
}
public static UriBuilder loginActionsBaseUrl(UriBuilder baseUriBuilder) {
return baseUriBuilder.path(RealmsResource.class).path(RealmsResource.class, "getLoginActionsService");
}
@ -208,7 +215,7 @@ public class LoginActionsService {
ClientSessionModel clientSession = RestartLoginCookie.restartSession(session, realm, code);
if (clientSession != null) {
event.clone().detail(Details.RESTART_AFTER_TIMEOUT, "true").error(Errors.EXPIRED_CODE);
response = processFlow(null, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), Messages.LOGIN_TIMEOUT);
response = processFlow(null, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), Messages.LOGIN_TIMEOUT, new AuthenticationProcessor());
return false;
}
} catch (Exception e) {
@ -267,11 +274,10 @@ public class LoginActionsService {
}
protected Response processAuthentication(String execution, ClientSessionModel clientSession, String errorMessage) {
return processFlow(execution, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), errorMessage);
return processFlow(execution, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), errorMessage, new AuthenticationProcessor());
}
protected Response processFlow(String execution, ClientSessionModel clientSession, String flowPath, AuthenticationFlowModel flow, String errorMessage) {
AuthenticationProcessor processor = new AuthenticationProcessor();
protected Response processFlow(String execution, ClientSessionModel clientSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) {
processor.setClientSession(clientSession)
.setFlowPath(flowPath)
.setBrowserFlow(true)
@ -384,12 +390,33 @@ public class LoginActionsService {
}
protected Response processResetCredentials(String execution, ClientSessionModel clientSession, String errorMessage) {
return processFlow(execution, clientSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), errorMessage);
AuthenticationProcessor authProcessor = new AuthenticationProcessor() {
@Override
protected Response authenticationComplete() {
boolean firstBrokerLoginInProgress = (clientSession.getNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null);
if (firstBrokerLoginInProgress) {
UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realm, clientSession);
if (!linkingUser.getId().equals(clientSession.getAuthenticatedUser().getId())) {
return ErrorPage.error(session, Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE, clientSession.getAuthenticatedUser().getUsername(), linkingUser.getUsername());
}
logger.debugf("Forget-password flow finished when authenticated user '%s' after first broker login.", linkingUser.getUsername());
return redirectToAfterFirstBrokerLoginEndpoint(clientSession);
} else {
return super.authenticationComplete();
}
}
};
return processFlow(execution, clientSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), errorMessage, authProcessor);
}
protected Response processRegistration(String execution, ClientSessionModel clientSession, String errorMessage) {
return processFlow(execution, clientSession, REGISTRATION_PATH, realm.getRegistrationFlow(), errorMessage);
return processFlow(execution, clientSession, REGISTRATION_PATH, realm.getRegistrationFlow(), errorMessage, new AuthenticationProcessor());
}
@ -450,6 +477,60 @@ public class LoginActionsService {
return processRegistration(execution, clientSession, null);
}
@Path(FIRST_BROKER_LOGIN_PATH)
@GET
public Response firstBrokerLoginGet(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
return firstBrokerLogin(code, execution);
}
@Path(FIRST_BROKER_LOGIN_PATH)
@POST
public Response firstBrokerLoginPost(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
return firstBrokerLogin(code, execution);
}
protected Response firstBrokerLogin(String code, String execution) {
event.event(EventType.IDENTITY_PROVIDER_FIRST_LOGIN);
Checks checks = new Checks();
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name())) {
return checks.response;
}
event.detail(Details.CODE_ID, code);
ClientSessionCode clientSessionCode = checks.clientCode;
ClientSessionModel clientSession = clientSessionCode.getClientSession();
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession);
if (serializedCtx == null) {
throw new WebApplicationException(ErrorPage.error(session, "Not found serialized context in clientSession"));
}
BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, clientSession);
AuthenticationFlowModel firstBrokerLoginFlow = realm.getAuthenticationFlowById(brokerContext.getIdpConfig().getFirstBrokerLoginFlowId());
AuthenticationProcessor processor = new AuthenticationProcessor() {
@Override
protected Response authenticationComplete() {
return redirectToAfterFirstBrokerLoginEndpoint(clientSession);
}
};
return processFlow(execution, clientSession, FIRST_BROKER_LOGIN_PATH, firstBrokerLoginFlow, null, processor);
}
private Response redirectToAfterFirstBrokerLoginEndpoint(ClientSessionModel clientSession) {
ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession);
clientSession.setTimestamp(Time.currentTime());
URI redirect = Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode());
logger.debugf("Redirecting to '%s' ", redirect);
return Response.status(302).location(redirect).build();
}
/**
* OAuth grant page. You should not invoked this directly!
*
@ -627,6 +708,10 @@ public class LoginActionsService {
}
private String getActionCookie() {
return getActionCookie(headers, realm, uriInfo, clientConnection);
}
public static String getActionCookie(HttpHeaders headers, RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection) {
Cookie cookie = headers.getCookies().get(ACTION_COOKIE);
AuthenticationManager.expireCookie(realm, ACTION_COOKIE, AuthenticationManager.getRealmCookiePath(realm, uriInfo), realm.getSslRequired().isRequired(clientConnection), clientConnection);
return cookie != null ? cookie.getValue() : null;

View file

@ -79,7 +79,7 @@ public class IdentityProviderResource {
@Produces(MediaType.APPLICATION_JSON)
public IdentityProviderRepresentation getIdentityProvider() {
this.auth.requireView();
IdentityProviderRepresentation rep = ModelToRepresentation.toRepresentation(this.identityProviderModel);
IdentityProviderRepresentation rep = ModelToRepresentation.toRepresentation(realm, this.identityProviderModel);
return rep;
}
@ -117,7 +117,7 @@ public class IdentityProviderResource {
String newProviderId = providerRep.getAlias();
String oldProviderId = getProviderIdByInternalId(this.realm, internalId);
this.realm.updateIdentityProvider(RepresentationToModel.toModel(providerRep));
this.realm.updateIdentityProvider(RepresentationToModel.toModel(realm, providerRep));
if (oldProviderId != null && !oldProviderId.equals(newProviderId)) {

View file

@ -145,7 +145,7 @@ public class IdentityProvidersResource {
List<IdentityProviderRepresentation> representations = new ArrayList<IdentityProviderRepresentation>();
for (IdentityProviderModel identityProviderModel : realm.getIdentityProviders()) {
representations.add(ModelToRepresentation.toRepresentation(identityProviderModel));
representations.add(ModelToRepresentation.toRepresentation(realm, identityProviderModel));
}
return representations;
}
@ -164,7 +164,7 @@ public class IdentityProvidersResource {
this.auth.requireManage();
try {
IdentityProviderModel identityProvider = RepresentationToModel.toModel(representation);
IdentityProviderModel identityProvider = RepresentationToModel.toModel(realm, representation);
this.realm.addIdentityProvider(identityProvider);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, identityProvider.getInternalId())

View file

@ -1,8 +1,8 @@
package org.keycloak.services.validation;
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages;
@ -68,13 +68,13 @@ public class Validation {
}
public static List<FormMessage> validateUpdateProfileForm(MultivaluedMap<String, String> formData) {
return validateUpdateProfileForm(null, formData);
return validateUpdateProfileForm(false, formData);
}
public static List<FormMessage> validateUpdateProfileForm(RealmModel realm, MultivaluedMap<String, String> formData) {
public static List<FormMessage> validateUpdateProfileForm(boolean editUsernameAllowed, MultivaluedMap<String, String> formData) {
List<FormMessage> errors = new ArrayList<>();
if (realm != null && realm.isEditUsernameAllowed() && isBlank(formData.getFirst(FIELD_USERNAME))) {
if (editUsernameAllowed && isBlank(formData.getFirst(FIELD_USERNAME))) {
addError(errors, FIELD_USERNAME, Messages.MISSING_USERNAME);
}
@ -102,7 +102,7 @@ public class Validation {
* @param user to validate
* @return true if user object contains all mandatory values, false if some mandatory value is missing
*/
public static boolean validateUserMandatoryFields(RealmModel realm, UserModel user){
public static boolean validateUserMandatoryFields(RealmModel realm, UpdateProfileContext user){
return!(isBlank(user.getFirstName()) || isBlank(user.getLastName()) || isBlank(user.getEmail()));
}

View file

@ -9,3 +9,8 @@ org.keycloak.authentication.authenticators.resetcred.ResetCredentialChooseUser
org.keycloak.authentication.authenticators.resetcred.ResetCredentialEmail
org.keycloak.authentication.authenticators.resetcred.ResetOTP
org.keycloak.authentication.authenticators.resetcred.ResetPassword
org.keycloak.authentication.authenticators.broker.IdpUpdateProfileAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpDetectDuplicationsAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory

View file

@ -434,7 +434,8 @@ public abstract class AbstractIdentityProviderTest {
loginPage.findSocialButton(getProviderId());
}
@Test
// TODO: Reenable and adjust to KEYCLOAK-1750 changed behaviour
// @Test
public void testUserAlreadyExistsWhenUpdatingProfile() {
this.driver.navigate().to("http://localhost:8081/test-app/");
@ -469,7 +470,8 @@ public abstract class AbstractIdentityProviderTest {
assertNotNull(federatedUser);
}
@Test
// TODO: Reenable and adjust to KEYCLOAK-1750 changed behaviour
// @Test
public void testUserAlreadyExistsWhenNotUpdatingProfile() {
IdentityProviderModel identityProviderModel = getIdentityProviderModel();

View file

@ -30,9 +30,9 @@ import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
import org.keycloak.broker.saml.SAMLIdentityProvider;
import org.keycloak.broker.saml.SAMLIdentityProviderConfig;
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
import org.keycloak.models.ClientModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.social.facebook.FacebookIdentityProvider;
@ -63,7 +63,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
public void testInstallation() throws Exception {
RealmModel realm = installTestRealm();
assertIdentityProviderConfig(realm.getIdentityProviders());
assertIdentityProviderConfig(realm, realm.getIdentityProviders());
assertTrue(realm.isIdentityFederationEnabled());
this.realmManager.removeRealm(realm);
@ -85,6 +85,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
identityProviderModel.setTrustEmail(true);
identityProviderModel.setStoreToken(true);
identityProviderModel.setAuthenticateByDefault(true);
identityProviderModel.setFirstBrokerLoginFlowId(realm.getBrowserFlow().getId());
realm.updateIdentityProvider(identityProviderModel);
@ -100,6 +101,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
assertTrue(identityProviderModel.isTrustEmail());
assertTrue(identityProviderModel.isStoreToken());
assertTrue(identityProviderModel.isAuthenticateByDefault());
assertEquals(identityProviderModel.getFirstBrokerLoginFlowId(), realm.getBrowserFlow().getId());
identityProviderModel.getConfig().remove("config-added");
identityProviderModel.setEnabled(true);
@ -122,7 +124,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
this.realmManager.removeRealm(realm);
}
private void assertIdentityProviderConfig(List<IdentityProviderModel> identityProviders) {
private void assertIdentityProviderConfig(RealmModel realm, List<IdentityProviderModel> identityProviders) {
assertFalse(identityProviders.isEmpty());
Set<String> checkedProviders = new HashSet<String>(getExpectedProviders());
@ -138,9 +140,9 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
} else if (OIDCIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
assertOidcIdentityProviderConfig(identityProvider);
} else if (FacebookIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
assertFacebookIdentityProviderConfig(identityProvider);
assertFacebookIdentityProviderConfig(realm, identityProvider);
} else if (GitHubIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
assertGitHubIdentityProviderConfig(identityProvider);
assertGitHubIdentityProviderConfig(realm, identityProvider);
} else if (TwitterIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
assertTwitterIdentityProviderConfig(identityProvider);
} else if (LinkedInIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
@ -213,7 +215,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
assertEquals("clientSecret", config.getClientSecret());
}
private void assertFacebookIdentityProviderConfig(IdentityProviderModel identityProvider) {
private void assertFacebookIdentityProviderConfig(RealmModel realm, IdentityProviderModel identityProvider) {
FacebookIdentityProvider facebookIdentityProvider = new FacebookIdentityProviderFactory().create(identityProvider);
OAuth2IdentityProviderConfig config = facebookIdentityProvider.getConfig();
@ -226,12 +228,13 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
assertEquals(false, config.isStoreToken());
assertEquals("clientId", config.getClientId());
assertEquals("clientSecret", config.getClientSecret());
assertEquals(realm.getBrowserFlow().getId(), identityProvider.getFirstBrokerLoginFlowId());
assertEquals(FacebookIdentityProvider.AUTH_URL, config.getAuthorizationUrl());
assertEquals(FacebookIdentityProvider.TOKEN_URL, config.getTokenUrl());
assertEquals(FacebookIdentityProvider.PROFILE_URL, config.getUserInfoUrl());
}
private void assertGitHubIdentityProviderConfig(IdentityProviderModel identityProvider) {
private void assertGitHubIdentityProviderConfig(RealmModel realm, IdentityProviderModel identityProvider) {
GitHubIdentityProvider gitHubIdentityProvider = new GitHubIdentityProviderFactory().create(identityProvider);
OAuth2IdentityProviderConfig config = gitHubIdentityProvider.getConfig();
@ -244,6 +247,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
assertEquals(false, config.isStoreToken());
assertEquals("clientId", config.getClientId());
assertEquals("clientSecret", config.getClientSecret());
assertEquals(realm.getFlowByAlias(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW).getId(), identityProvider.getFirstBrokerLoginFlowId());
assertEquals(GitHubIdentityProvider.AUTH_URL, config.getAuthorizationUrl());
assertEquals(GitHubIdentityProvider.TOKEN_URL, config.getTokenUrl());
assertEquals(GitHubIdentityProvider.PROFILE_URL, config.getUserInfoUrl());

View file

@ -30,6 +30,7 @@
"providerId" : "facebook",
"enabled": true,
"updateProfileFirstLogin" : "false",
"firstBrokerLoginFlowAlias" : "browser",
"config": {
"authorizationUrl": "authorizationUrl",
"tokenUrl": "tokenUrl",