diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
index ffe8328baa..b1b5890d6e 100644
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -113,6 +113,7 @@ role_read-token=Read token
role_offline-access=Offline access
client_account=Account
client_security-admin-console=Security Admin Console
+client_admin-cli=Admin CLI
client_realm-management=Realm Management
client_broker=Broker
diff --git a/forms/email-freemarker/pom.xml b/forms/email-freemarker/pom.xml
index adc07dc73a..5a5b4d274e 100755
--- a/forms/email-freemarker/pom.xml
+++ b/forms/email-freemarker/pom.xml
@@ -54,11 +54,6 @@
freemarker
provided
-
- javax.mail
- mail
- provided
-
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java
index b2d6b1dcf5..0783529ee5 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java
@@ -47,8 +47,9 @@ public class ProfileBean {
this.user = user;
this.formData = formData;
- if (user.getAttributes() != null) {
- for (Map.Entry> attr : user.getAttributes().entrySet()) {
+ Map> modelAttrs = user.getAttributes();
+ if (modelAttrs != null) {
+ for (Map.Entry> attr : modelAttrs.entrySet()) {
List attrValue = attr.getValue();
if (attrValue != null && attrValue.size() > 0) {
attributes.put(attr.getKey(), attrValue.get(0));
diff --git a/model/api/src/main/java/org/keycloak/migration/MigrationProvider.java b/model/api/src/main/java/org/keycloak/migration/MigrationProvider.java
index 9c8c67892e..f83e336f9f 100755
--- a/model/api/src/main/java/org/keycloak/migration/MigrationProvider.java
+++ b/model/api/src/main/java/org/keycloak/migration/MigrationProvider.java
@@ -1,6 +1,7 @@
package org.keycloak.migration;
import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
@@ -21,4 +22,6 @@ public interface MigrationProvider extends Provider {
List getBuiltinMappers(String protocol);
+ void setupAdminCli(RealmModel realm);
+
}
diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_7_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_7_0.java
index e4ead4dbb2..2c5710d615 100644
--- a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_7_0.java
+++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_7_0.java
@@ -2,10 +2,14 @@ package org.keycloak.migration.migrators;
import java.util.List;
+import org.keycloak.migration.MigrationProvider;
import org.keycloak.migration.ModelVersion;
+import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.Constants;
+import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.DefaultAuthenticationFlows;
/**
* @author Marek Posolda
@@ -17,7 +21,24 @@ public class MigrateTo1_7_0 {
public void migrate(KeycloakSession session) {
List realms = session.realms().getRealms();
for (RealmModel realm : realms) {
+ // Set default accessToken timeout for implicit flow
realm.setAccessTokenLifespanForImplicitFlow(Constants.DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT);
+
+ // Add 'admin-cli' builtin client
+ MigrationProvider migrationProvider = session.getProvider(MigrationProvider.class);
+ migrationProvider.setupAdminCli(realm);
+
+ // add firstBrokerLogin flow and set it to all identityProviders
+ DefaultAuthenticationFlows.migrateFlows(realm);
+ AuthenticationFlowModel firstBrokerLoginFlow = realm.getFlowByAlias(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW);
+
+ List identityProviders = realm.getIdentityProviders();
+ for (IdentityProviderModel identityProvider : identityProviders) {
+ if (identityProvider.getFirstBrokerLoginFlowId() == null) {
+ identityProvider.setFirstBrokerLoginFlowId(firstBrokerLoginFlow.getId());
+ realm.updateIdentityProvider(identityProvider);
+ }
+ }
}
}
}
diff --git a/model/api/src/main/java/org/keycloak/models/Constants.java b/model/api/src/main/java/org/keycloak/models/Constants.java
index c50e4cd89b..0c529293f2 100755
--- a/model/api/src/main/java/org/keycloak/models/Constants.java
+++ b/model/api/src/main/java/org/keycloak/models/Constants.java
@@ -8,6 +8,7 @@ import org.keycloak.OAuth2Constants;
*/
public interface Constants {
String ADMIN_CONSOLE_CLIENT_ID = "security-admin-console";
+ String ADMIN_CLI_CLIENT_ID = "admin-cli";
String ACCOUNT_MANAGEMENT_CLIENT_ID = "account";
String IMPERSONATION_SERVICE_CLIENT_ID = "impersonation";
@@ -27,4 +28,7 @@ public interface Constants {
String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
String KEY = "key";
+
+ // Prefix for user attributes used in various "context"data maps
+ public static final String USER_ATTRIBUTES_PREFIX = "user.attributes.";
}
diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
index b5edbaf9ee..46259c9198 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
@@ -34,7 +34,6 @@ public class ClientEntity extends AbstractIdentifiableEntity {
private boolean implicitFlowEnabled;
private boolean directAccessGrantsEnabled;
private boolean serviceAccountsEnabled;
- private boolean directGrantsOnly;
private int nodeReRegistrationTimeout;
// We are using names of defaultRoles (not ids)
@@ -278,14 +277,6 @@ public class ClientEntity extends AbstractIdentifiableEntity {
this.serviceAccountsEnabled = serviceAccountsEnabled;
}
- public boolean isDirectGrantsOnly() {
- return directGrantsOnly;
- }
-
- public void setDirectGrantsOnly(boolean directGrantsOnly) {
- this.directGrantsOnly = directGrantsOnly;
- }
-
public List getDefaultRoles() {
return defaultRoles;
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
index 40a8999910..3a105c46e2 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
@@ -429,6 +429,10 @@ public class DefaultAuthenticationFlows {
if (migrate) {
// Try to read OTP requirement from browser flow
AuthenticationFlowModel browserFlow = realm.getBrowserFlow();
+ if (browserFlow == null) {
+ browserFlow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW);
+ }
+
List browserExecutions = new LinkedList<>();
KeycloakModelUtils.deepFindAuthenticationExecutions(realm, browserFlow, browserExecutions);
for (AuthenticationExecutionModel browserExecution : browserExecutions) {
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 8360e48731..dfa2e46190 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -460,6 +460,10 @@ public class RepresentationToModel {
newRealm.setClientAuthenticationFlow(newRealm.getFlowByAlias(rep.getClientAuthenticationFlow()));
}
+ // Added in 1.7
+ if (newRealm.getFlowByAlias(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW) == null) {
+ DefaultAuthenticationFlows.firstBrokerLoginFlow(newRealm, true);
+ }
}
private static void convertDeprecatedSocialProviders(RealmRepresentation rep) {
@@ -776,17 +780,19 @@ public class RepresentationToModel {
if (resourceRep.getBaseUrl() != null) client.setBaseUrl(resourceRep.getBaseUrl());
if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly());
if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired());
- if (resourceRep.isStandardFlowEnabled() != null) client.setStandardFlowEnabled(resourceRep.isStandardFlowEnabled());
- if (resourceRep.isImplicitFlowEnabled() != null) client.setImplicitFlowEnabled(resourceRep.isImplicitFlowEnabled());
- if (resourceRep.isDirectAccessGrantsEnabled() != null) client.setDirectAccessGrantsEnabled(resourceRep.isDirectAccessGrantsEnabled());
- if (resourceRep.isServiceAccountsEnabled() != null) client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled());
// Backwards compatibility only
if (resourceRep.isDirectGrantsOnly() != null) {
logger.warn("Using deprecated 'directGrantsOnly' configuration in JSON representation. It will be removed in future versions");
client.setStandardFlowEnabled(!resourceRep.isDirectGrantsOnly());
+ client.setDirectAccessGrantsEnabled(resourceRep.isDirectGrantsOnly());
}
+ if (resourceRep.isStandardFlowEnabled() != null) client.setStandardFlowEnabled(resourceRep.isStandardFlowEnabled());
+ if (resourceRep.isImplicitFlowEnabled() != null) client.setImplicitFlowEnabled(resourceRep.isImplicitFlowEnabled());
+ if (resourceRep.isDirectAccessGrantsEnabled() != null) client.setDirectAccessGrantsEnabled(resourceRep.isDirectAccessGrantsEnabled());
+ if (resourceRep.isServiceAccountsEnabled() != null) client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled());
+
if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient());
if (resourceRep.isFrontchannelLogout() != null) client.setFrontchannelLogout(resourceRep.isFrontchannelLogout());
if (resourceRep.getProtocol() != null) client.setProtocol(resourceRep.getProtocol());
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java
index 87fe25b3dc..4eb0bdf8ee 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java
@@ -112,21 +112,25 @@ public class InfinispanCacheUserProviderFactory implements CacheUserProviderFact
@CacheEntryRemoved
public void userRemoved(CacheEntryRemovedEvent event) {
- CachedUser user = event.getOldValue();
- if (event.isPre() && user != null) {
- removeUser(user);
+ if (event.isPre()) {
+ CachedUser user = event.getValue();
+ if (user != null) {
+ removeUser(user);
- log.tracev("User invalidated realm={0}, id={1}, username={2}", user.getRealm(), user.getId(), user.getUsername());
+ log.tracev("User invalidated realm={0}, id={1}, username={2}", user.getRealm(), user.getId(), user.getUsername());
+ }
}
}
@CacheEntryInvalidated
public void userInvalidated(CacheEntryInvalidatedEvent event) {
- CachedUser user = event.getValue();
- if (event.isPre() && user != null) {
- removeUser(user);
+ if (event.isPre()) {
+ CachedUser user = event.getValue();
+ if (user != null) {
+ removeUser(user);
- log.tracev("User invalidated realm={0}, id={1}, username={2}", user.getRealm(), user.getId(), user.getUsername());
+ log.tracev("User invalidated realm={0}, id={1}, username={2}", user.getRealm(), user.getId(), user.getUsername());
+ }
}
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index cb69f14d62..3f5817956b 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -726,7 +726,6 @@ public class RealmAdapter implements RealmModel {
entity.setClientId(clientId);
entity.setEnabled(true);
entity.setStandardFlowEnabled(true);
- entity.setDirectAccessGrantsEnabled(true);
entity.setRealm(realm);
realm.getClients().add(entity);
em.persist(entity);
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index c8fc1f0b95..fc840ba534 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -811,7 +811,6 @@ public class RealmAdapter extends AbstractMongoAdapter impleme
clientEntity.setRealmId(getId());
clientEntity.setEnabled(true);
clientEntity.setStandardFlowEnabled(true);
- clientEntity.setDirectAccessGrantsEnabled(true);
getMongoStore().insertEntity(clientEntity, invocationContext);
final ClientModel model = new ClientAdapter(session, this, clientEntity, invocationContext);
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
index 3a30c2ee6f..0bc3edeea0 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
@@ -522,7 +522,7 @@ public class SamlProtocol implements LoginProtocol {
logger.debug("finishLogout");
String logoutBindingUri = userSession.getNote(SAML_LOGOUT_BINDING_URI);
if (logoutBindingUri == null) {
- logger.error("Can't finish SAML logout as there is no logout binding set");
+ logger.error("Can't finish SAML logout as there is no logout binding set. Please configure the logout service url in the admin console for your client applications.");
return ErrorPage.error(session, Messages.FAILED_LOGOUT);
}
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java
index 1ee3cd2bff..10ef0191c4 100644
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java
@@ -5,6 +5,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
+import org.keycloak.services.clientregistration.ClientRegistrationException;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
diff --git a/services/pom.xml b/services/pom.xml
index bf2ee80b50..105a2cc976 100755
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -50,6 +50,10 @@
org.keycloak
keycloak-email-api
+
+ javax.mail
+ mail
+
org.keycloak
keycloak-login-api
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java
index b4ee957e43..f4da7e97d7 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java
@@ -41,13 +41,21 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator
return;
}
- ExistingUserInfo duplication = checkExistingUser(context, serializedCtx, brokerContext);
+ String username = getUsername(context, serializedCtx, brokerContext);
+ if (username == null) {
+ logger.warnf("%s is null. Reset flow and enforce showing reviewProfile page", realm.isRegistrationEmailAsUsername() ? "Email" : "Username");
+ context.getClientSession().setNote(ENFORCE_UPDATE_PROFILE, "true");
+ context.resetFlow();
+ return;
+ }
+
+ ExistingUserInfo duplication = checkExistingUser(context, username, 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());
+ username, brokerContext.getIdpConfig().getAlias());
- UserModel federatedUser = session.users().addUser(realm, brokerContext.getModelUsername());
+ UserModel federatedUser = session.users().addUser(realm, username);
federatedUser.setEnabled(true);
federatedUser.setEmail(brokerContext.getEmail());
federatedUser.setFirstName(brokerContext.getFirstName());
@@ -92,7 +100,7 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator
}
// Could be overriden to detect duplication based on other criterias (firstName, lastName, ...)
- protected ExistingUserInfo checkExistingUser(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
+ protected ExistingUserInfo checkExistingUser(AuthenticationFlowContext context, String username, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
if (brokerContext.getEmail() != null) {
UserModel existingUser = context.getSession().users().getUserByEmail(brokerContext.getEmail(), context.getRealm());
@@ -101,7 +109,7 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator
}
}
- UserModel existingUser = context.getSession().users().getUserByUsername(brokerContext.getModelUsername(), context.getRealm());
+ UserModel existingUser = context.getSession().users().getUserByUsername(username, context.getRealm());
if (existingUser != null) {
return new ExistingUserInfo(existingUser.getId(), UserModel.USERNAME, existingUser.getUsername());
}
@@ -109,6 +117,11 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator
return null;
}
+ protected String getUsername(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
+ RealmModel realm = context.getRealm();
+ return realm.isRegistrationEmailAsUsername() ? brokerContext.getEmail() : brokerContext.getModelUsername();
+ }
+
@Override
public boolean requiresUser() {
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java
index 04eb77b520..a4d0ea345c 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java
@@ -83,7 +83,7 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
RealmModel realm = context.getRealm();
- List errors = Validation.validateUpdateProfileForm(true, formData);
+ List errors = Validation.validateUpdateProfileForm(!realm.isRegistrationEmailAsUsername(), formData);
if (errors != null && !errors.isEmpty()) {
Response challenge = context.form()
.setErrors(errors)
@@ -94,7 +94,8 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
return;
}
- userCtx.setUsername(formData.getFirst(UserModel.USERNAME));
+ String username = realm.isRegistrationEmailAsUsername() ? formData.getFirst(UserModel.EMAIL) : formData.getFirst(UserModel.USERNAME);
+ userCtx.setUsername(username);
userCtx.setFirstName(formData.getFirst(UserModel.FIRST_NAME));
userCtx.setLastName(formData.getFirst(UserModel.LAST_NAME));
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticatorFactory.java
index e10c924c5a..fc472d0b09 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticatorFactory.java
@@ -94,7 +94,8 @@ public class IdpReviewProfileAuthenticatorFactory implements AuthenticatorFactor
property.setDefaultValue(updateProfileValues);
property.setHelpText("Define conditions under which a user has to review and update his profile after first-time login. Value 'On' means that"
+ " page for reviewing profile will be displayed and user can review and update his profile. Value 'off' means that page won't be displayed."
- + " Value 'missing' means that page is displayed just when some required attribute is missing (wasn't downloaded from identity provider). Value 'missing' is the default one");
+ + " Value 'missing' means that page is displayed just when some required attribute is missing (wasn't downloaded from identity provider). Value 'missing' is the default one."
+ + " WARN: In case that user clicks 'Review profile info' on link duplications page, the update page will be always displayed. You would need to disable this authenticator to never display the page.");
configProperties.add(property);
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java
index 8f1b026f0c..968831c1b4 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java
@@ -1,6 +1,8 @@
package org.keycloak.authentication.authenticators.broker.util;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -14,6 +16,7 @@ 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.Constants;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
@@ -37,13 +40,16 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
private String code;
private String token;
+ @JsonIgnore
+ private boolean emailAsUsername;
+
private String identityProviderId;
private Map contextData = new HashMap<>();
@JsonIgnore
@Override
public boolean isEditUsernameAllowed() {
- return true;
+ return !emailAsUsername;
}
public String getId() {
@@ -159,44 +165,52 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
this.contextData = contextData;
}
+ @JsonIgnore
@Override
public Map> getAttributes() {
Map> result = new HashMap<>();
for (Map.Entry entry : this.contextData.entrySet()) {
- if (entry.getKey().startsWith("user.attributes.")) {
- ContextDataEntry ctxEntry = entry.getValue();
- String asString = ctxEntry.getData();
- try {
- List asList = JsonSerialization.readValue(asString, List.class);
- result.put(entry.getKey().substring(16), asList);
- } catch (IOException ioe) {
- throw new RuntimeException(ioe);
- }
+ if (entry.getKey().startsWith(Constants.USER_ATTRIBUTES_PREFIX)) {
+ String attrName = entry.getKey().substring(16); // length of USER_ATTRIBUTES_PREFIX
+ List asList = getAttribute(attrName);
+ result.put(attrName, asList);
}
}
return result;
}
+ @JsonIgnore
+ @Override
+ public void setSingleAttribute(String name, String value) {
+ List list = new ArrayList<>();
+ list.add(value);
+ setAttribute(name, list);
+ }
+
+ @JsonIgnore
@Override
public void setAttribute(String key, List value) {
try {
- String listStr = JsonSerialization.writeValueAsString(value);
+ byte[] listBytes = JsonSerialization.writeValueAsBytes(value);
+ String listStr = Base64Url.encode(listBytes);
ContextDataEntry ctxEntry = ContextDataEntry.create(List.class.getName(), listStr);
- this.contextData.put("user.attributes." + key, ctxEntry);
+ this.contextData.put(Constants.USER_ATTRIBUTES_PREFIX + key, ctxEntry);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
+ @JsonIgnore
@Override
public List getAttribute(String key) {
- ContextDataEntry ctxEntry = this.contextData.get("user.attributes." + key);
+ ContextDataEntry ctxEntry = this.contextData.get(Constants.USER_ATTRIBUTES_PREFIX + key);
if (ctxEntry != null) {
try {
String asString = ctxEntry.getData();
- List asList = JsonSerialization.readValue(asString, List.class);
+ byte[] asBytes = Base64Url.decode(asString);
+ List asList = JsonSerialization.readValue(asBytes, List.class);
return asList;
} catch (IOException ioe) {
throw new RuntimeException(ioe);
@@ -206,6 +220,17 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
}
}
+ @JsonIgnore
+ @Override
+ public String getFirstAttribute(String name) {
+ List attrs = getAttribute(name);
+ if (attrs == null || attrs.isEmpty()) {
+ return null;
+ } else {
+ return attrs.get(0);
+ }
+ }
+
public BrokeredIdentityContext deserialize(KeycloakSession session, ClientSessionModel clientSession) {
BrokeredIdentityContext ctx = new BrokeredIdentityContext(getId());
@@ -261,6 +286,8 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
ctx.setToken(context.getToken());
ctx.setIdentityProviderId(context.getIdpConfig().getAlias());
+ ctx.emailAsUsername = context.getClientSession().getRealm().isRegistrationEmailAsUsername();
+
IdentityProviderDataMarshaller serializer = context.getIdp().getMarshaller();
for (Map.Entry entry : context.getContextData().entrySet()) {
@@ -289,7 +316,9 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
return null;
} else {
try {
- return JsonSerialization.readValue(asString, SerializedBrokeredIdentityContext.class);
+ SerializedBrokeredIdentityContext serializedCtx = JsonSerialization.readValue(asString, SerializedBrokeredIdentityContext.class);
+ serializedCtx.emailAsUsername = clientSession.getRealm().isRegistrationEmailAsUsername();
+ return serializedCtx;
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java
index 2acef37a0e..a88d173cac 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java
@@ -31,8 +31,12 @@ public interface UpdateProfileContext {
Map> getAttributes();
+ void setSingleAttribute(String name, String value);
+
void setAttribute(String key, List value);
+ String getFirstAttribute(String name);
+
List getAttribute(String key);
}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java
index 55d6ddae46..94a1151855 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java
@@ -69,11 +69,21 @@ public class UserUpdateProfileContext implements UpdateProfileContext {
return user.getAttributes();
}
+ @Override
+ public void setSingleAttribute(String name, String value) {
+ user.setSingleAttribute(name, value);
+ }
+
@Override
public void setAttribute(String key, List value) {
user.setAttribute(key, value);
}
+ @Override
+ public String getFirstAttribute(String name) {
+ return user.getFirstAttribute(name);
+ }
+
@Override
public List getAttribute(String key) {
return user.getAttribute(key);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
index 60e9f9d36a..98fb49e08d 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
@@ -4,6 +4,7 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.services.clientregistration.ClientRegistrationService;
import org.keycloak.services.clientregistration.oidc.OIDCClientRegistrationProviderFactory;
import org.keycloak.services.resources.RealmsResource;
@@ -22,13 +23,13 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
public static final List DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list("RS256");
- public static final List DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS);
+ public static final List DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS);
- public static final List DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE);
+ public static final List DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE, OIDCResponseType.NONE, OIDCResponseType.ID_TOKEN, "id_token token", "code id_token", "code token", "code id_token token");
public static final List DEFAULT_SUBJECT_TYPES_SUPPORTED = list("public");
- public static final List DEFAULT_RESPONSE_MODES_SUPPORTED = list("query");
+ public static final List DEFAULT_RESPONSE_MODES_SUPPORTED = list("query", "fragment", "form_post");
private KeycloakSession session;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index fe358e0ba4..178624b63d 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -178,6 +178,8 @@ public class TokenEndpoint {
} else {
throw new ErrorResponseException(Errors.INVALID_REQUEST, "Invalid " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST);
}
+
+ event.detail(Details.GRANT_TYPE, grantType);
}
public Response buildAuthorizationCodeAccessTokenResponse() {
@@ -223,6 +225,11 @@ public class TokenEndpoint {
throw new ErrorResponseException("invalid_grant", "Auth error", Response.Status.BAD_REQUEST);
}
+ if (!client.isStandardFlowEnabled()) {
+ event.error(Errors.NOT_ALLOWED);
+ throw new ErrorResponseException("invalid_grant", "Client not allowed to exchange code", Response.Status.BAD_REQUEST);
+ }
+
UserModel user = session.users().getUserById(userSession.getUser().getId(), realm);
if (user == null) {
event.error(Errors.USER_NOT_FOUND);
@@ -327,7 +334,12 @@ public class TokenEndpoint {
}
public Response buildResourceOwnerPasswordCredentialsGrant() {
- event.detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD);
+ event.detail(Details.AUTH_METHOD, "oauth_credentials");
+
+ if (!client.isDirectAccessGrantsEnabled()) {
+ event.error(Errors.NOT_ALLOWED);
+ throw new ErrorResponseException("invalid_grant", "Client not allowed for direct access grants", Response.Status.BAD_REQUEST);
+ }
if (client.isConsentRequired()) {
event.error(Errors.CONSENT_DENIED);
@@ -393,8 +405,6 @@ public class TokenEndpoint {
throw new ErrorResponseException("unauthorized_client", "Client not enabled to retrieve service account", Response.Status.UNAUTHORIZED);
}
- event.detail(Details.RESPONSE_TYPE, OAuth2Constants.CLIENT_CREDENTIALS);
-
UserModel clientUser = session.users().getUserByServiceAccountClient(client);
if (clientUser == null || client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java
index 06cc3cc91c..4340229231 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java
@@ -42,7 +42,7 @@ public abstract class OIDCRedirectUriBuilder {
// http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
- public static class QueryRedirectUriBuilder extends OIDCRedirectUriBuilder {
+ private static class QueryRedirectUriBuilder extends OIDCRedirectUriBuilder {
protected QueryRedirectUriBuilder(KeycloakUriBuilder uriBuilder) {
super(uriBuilder);
@@ -64,7 +64,7 @@ public abstract class OIDCRedirectUriBuilder {
// http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
- public static class FragmentRedirectUriBuilder extends OIDCRedirectUriBuilder {
+ private static class FragmentRedirectUriBuilder extends OIDCRedirectUriBuilder {
private StringBuilder fragment;
@@ -98,7 +98,7 @@ public abstract class OIDCRedirectUriBuilder {
// http://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html
- public static class FormPostRedirectUriBuilder extends OIDCRedirectUriBuilder {
+ private static class FormPostRedirectUriBuilder extends OIDCRedirectUriBuilder {
private Map params = new HashMap<>();
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseType.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseType.java
index 6377b22a47..9313aa60bc 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseType.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseType.java
@@ -46,6 +46,16 @@ public class OIDCResponseType {
return new OIDCResponseType(allowedTypes);
}
+ public static OIDCResponseType parse(List responseTypes) {
+ OIDCResponseType result = new OIDCResponseType(new ArrayList());
+ for (String respType : responseTypes) {
+ OIDCResponseType responseType = parse(respType);
+ result.responseTypes.addAll(responseType.responseTypes);
+ }
+
+ return result;
+ }
+
private static void validateAllowedTypes(List responseTypes) {
if (responseTypes.size() == 0) {
throw new IllegalStateException("No responseType provided");
@@ -53,9 +63,6 @@ public class OIDCResponseType {
if (responseTypes.contains(NONE) && responseTypes.size() > 1) {
throw new IllegalArgumentException("None not allowed with some other response_type");
}
- if (responseTypes.contains(ID_TOKEN) && responseTypes.size() == 1) {
- throw new IllegalArgumentException("Not supported to use response_type=id_token alone");
- }
if (responseTypes.contains(TOKEN) && responseTypes.size() == 1) {
throw new IllegalArgumentException("Not supported to use response_type=token alone");
}
@@ -72,7 +79,7 @@ public class OIDCResponseType {
}
public boolean isImplicitFlow() {
- return hasResponseType(TOKEN) && hasResponseType(ID_TOKEN) && !hasResponseType(CODE);
+ return hasResponseType(ID_TOKEN) && !hasResponseType(CODE);
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationException.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationException.java
new file mode 100644
index 0000000000..71c9c3d077
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationException.java
@@ -0,0 +1,23 @@
+package org.keycloak.services.clientregistration;
+
+/**
+ * @author Marek Posolda
+ */
+public class ClientRegistrationException extends RuntimeException {
+
+ public ClientRegistrationException() {
+ super();
+ }
+
+ public ClientRegistrationException(String message) {
+ super(message);
+ }
+
+ public ClientRegistrationException(Throwable throwable) {
+ super(throwable);
+ }
+
+ public ClientRegistrationException(String message, Throwable throwable) {
+ super(message, throwable);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
index a7f9f2c82f..594a5f061f 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
@@ -1,21 +1,48 @@
package org.keycloak.services.clientregistration.oidc;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
+import org.keycloak.services.clientregistration.ClientRegistrationException;
import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
/**
* @author Stian Thorgersen
*/
public class DescriptionConverter {
- public static ClientRepresentation toInternal(OIDCClientRepresentation clientOIDC) {
+ public static ClientRepresentation toInternal(OIDCClientRepresentation clientOIDC) throws ClientRegistrationException {
ClientRepresentation client = new ClientRepresentation();
client.setClientId(clientOIDC.getClientId());
client.setName(clientOIDC.getClientName());
client.setRedirectUris(clientOIDC.getRedirectUris());
client.setBaseUrl(clientOIDC.getClientUri());
+
+ List oidcResponseTypes = clientOIDC.getResponseTypes();
+ if (oidcResponseTypes == null || oidcResponseTypes.isEmpty()) {
+ oidcResponseTypes = Collections.singletonList(OIDCResponseType.CODE);
+ }
+ List oidcGrantTypes = clientOIDC.getGrantTypes();
+
+ try {
+ OIDCResponseType responseType = OIDCResponseType.parse(oidcResponseTypes);
+ client.setStandardFlowEnabled(responseType.hasResponseType(OIDCResponseType.CODE));
+ client.setImplicitFlowEnabled(responseType.isImplicitOrHybridFlow());
+ if (oidcGrantTypes != null) {
+ client.setDirectAccessGrantsEnabled(oidcGrantTypes.contains(OAuth2Constants.PASSWORD));
+ client.setServiceAccountsEnabled(oidcGrantTypes.contains(OAuth2Constants.CLIENT_CREDENTIALS));
+ }
+ } catch (IllegalArgumentException iae) {
+ throw new ClientRegistrationException(iae.getMessage(), iae);
+ }
+
return client;
}
@@ -28,7 +55,45 @@ public class DescriptionConverter {
response.setRedirectUris(client.getRedirectUris());
response.setRegistrationAccessToken(client.getRegistrationAccessToken());
response.setRegistrationClientUri(uri.toString());
+ response.setResponseTypes(getOIDCResponseTypes(client));
+ response.setGrantTypes(getOIDCGrantTypes(client));
return response;
}
+ private static List getOIDCResponseTypes(ClientRepresentation client) {
+ List responseTypes = new ArrayList<>();
+ if (client.isStandardFlowEnabled()) {
+ responseTypes.add(OAuth2Constants.CODE);
+ responseTypes.add(OIDCResponseType.NONE);
+ }
+ if (client.isImplicitFlowEnabled()) {
+ responseTypes.add(OIDCResponseType.ID_TOKEN);
+ responseTypes.add("id_token token");
+ }
+ if (client.isStandardFlowEnabled() && client.isImplicitFlowEnabled()) {
+ responseTypes.add("code id_token");
+ responseTypes.add("code token");
+ responseTypes.add("code id_token token");
+ }
+ return responseTypes;
+ }
+
+ private static List getOIDCGrantTypes(ClientRepresentation client) {
+ List grantTypes = new ArrayList<>();
+ if (client.isStandardFlowEnabled()) {
+ grantTypes.add(OAuth2Constants.AUTHORIZATION_CODE);
+ }
+ if (client.isImplicitFlowEnabled()) {
+ grantTypes.add(OAuth2Constants.IMPLICIT);
+ }
+ if (client.isDirectAccessGrantsEnabled()) {
+ grantTypes.add(OAuth2Constants.PASSWORD);
+ }
+ if (client.isServiceAccountsEnabled()) {
+ grantTypes.add(OAuth2Constants.CLIENT_CREDENTIALS);
+ }
+ grantTypes.add(OAuth2Constants.REFRESH_TOKEN);
+ return grantTypes;
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java
index e60720bc87..82b8825eb9 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java
@@ -1,5 +1,6 @@
package org.keycloak.services.clientregistration.oidc;
+import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
@@ -9,6 +10,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
import org.keycloak.services.clientregistration.ClientRegistrationAuth;
+import org.keycloak.services.clientregistration.ClientRegistrationException;
import org.keycloak.services.clientregistration.ErrorCodes;
import javax.ws.rs.*;
@@ -21,6 +23,8 @@ import java.net.URI;
*/
public class OIDCClientRegistrationProvider extends AbstractClientRegistrationProvider {
+ private static final Logger log = Logger.getLogger(OIDCClientRegistrationProvider.class);
+
public OIDCClientRegistrationProvider(KeycloakSession session) {
super(session);
}
@@ -33,12 +37,17 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client Identifier included", Response.Status.BAD_REQUEST);
}
- ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
- client = create(client);
- URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
- clientOIDC = DescriptionConverter.toExternalResponse(client, uri);
- clientOIDC.setClientIdIssuedAt(Time.currentTime());
- return Response.created(uri).entity(clientOIDC).build();
+ try {
+ ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
+ client = create(client);
+ URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
+ clientOIDC = DescriptionConverter.toExternalResponse(client, uri);
+ clientOIDC.setClientIdIssuedAt(Time.currentTime());
+ return Response.created(uri).entity(clientOIDC).build();
+ } catch (ClientRegistrationException cre) {
+ log.error(cre.getMessage());
+ throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client metadata invalid", Response.Status.BAD_REQUEST);
+ }
}
@GET
@@ -54,11 +63,16 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
@Path("{clientId}")
@Consumes(MediaType.APPLICATION_JSON)
public Response updateOIDC(@PathParam("clientId") String clientId, OIDCClientRepresentation clientOIDC) {
- ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
- client = update(clientId, client);
- URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
- clientOIDC = DescriptionConverter.toExternalResponse(client, uri);
- return Response.ok(clientOIDC).build();
+ try {
+ ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
+ client = update(clientId, client);
+ URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
+ clientOIDC = DescriptionConverter.toExternalResponse(client, uri);
+ return Response.ok(clientOIDC).build();
+ } catch (ClientRegistrationException cre) {
+ log.error(cre.getMessage());
+ throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client metadata invalid", Response.Status.BAD_REQUEST);
+ }
}
@DELETE
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index d44a622828..8ab0cfc1c8 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -113,6 +113,7 @@ public class RealmManager implements RealmImporter {
setupAccountManagement(realm);
setupBrokerService(realm);
setupAdminConsole(realm);
+ setupAdminCli(realm);
setupImpersonationService(realm);
setupAuthenticationFlows(realm);
setupRequiredActions(realm);
@@ -158,6 +159,30 @@ public class RealmManager implements RealmImporter {
adminConsole.addScopeMapping(adminRole);
}
+ public void setupAdminCli(RealmModel realm) {
+ ClientModel adminCli = realm.getClientByClientId(Constants.ADMIN_CLI_CLIENT_ID);
+ if (adminCli == null) {
+ adminCli = new ClientManager(this).createClient(realm, Constants.ADMIN_CLI_CLIENT_ID);
+ adminCli.setName("${client_" + Constants.ADMIN_CLI_CLIENT_ID + "}");
+ adminCli.setEnabled(true);
+ adminCli.setPublicClient(true);
+ adminCli.setFullScopeAllowed(false);
+ adminCli.setStandardFlowEnabled(false);
+ adminCli.setDirectAccessGrantsEnabled(true);
+
+ RoleModel adminRole;
+ if (realm.getName().equals(Config.getAdminRealm())) {
+ adminRole = realm.getRole(AdminRoles.ADMIN);
+ } else {
+ String realmAdminApplicationClientId = getRealmAdminClientId(realm);
+ ClientModel realmAdminApp = realm.getClientByClientId(realmAdminApplicationClientId);
+ adminRole = realmAdminApp.getRole(AdminRoles.REALM_ADMIN);
+ }
+ adminCli.addScopeMapping(adminRole);
+ }
+
+ }
+
public String getRealmAdminClientId(RealmModel realm) {
return Constants.REALM_MANAGEMENT_CLIENT_ID;
}
@@ -375,6 +400,16 @@ public class RealmManager implements RealmImporter {
if (!hasBrokerClient(rep)) setupBrokerService(realm);
if (!hasAdminConsoleClient(rep)) setupAdminConsole(realm);
+
+ boolean postponeAdminCliSetup = false;
+ if (!hasAdminCliClient(rep)) {
+ if (hasRealmAdminManagementClient(rep)) {
+ postponeAdminCliSetup = true;
+ } else {
+ setupAdminCli(realm);
+ }
+ }
+
if (!hasRealmRole(rep, Constants.OFFLINE_ACCESS_ROLE)) setupOfflineTokens(realm);
RepresentationToModel.importRealm(session, rep, realm);
@@ -389,6 +424,10 @@ public class RealmManager implements RealmImporter {
setupImpersonationService(realm);
}
+ if (postponeAdminCliSetup) {
+ setupAdminCli(realm);
+ }
+
setupAuthenticationFlows(realm);
setupRequiredActions(realm);
@@ -428,6 +467,10 @@ public class RealmManager implements RealmImporter {
return hasClient(rep, Constants.ADMIN_CONSOLE_CLIENT_ID);
}
+ private boolean hasAdminCliClient(RealmRepresentation rep) {
+ return hasClient(rep, Constants.ADMIN_CLI_CLIENT_ID);
+ }
+
private boolean hasClient(RealmRepresentation rep, String clientId) {
if (rep.getClients() != null) {
for (ClientRepresentation clientRep : rep.getClients()) {
diff --git a/services/src/main/java/org/keycloak/services/migration/DefaultMigrationProvider.java b/services/src/main/java/org/keycloak/services/migration/DefaultMigrationProvider.java
index ad46afe1dc..e1e6f23049 100644
--- a/services/src/main/java/org/keycloak/services/migration/DefaultMigrationProvider.java
+++ b/services/src/main/java/org/keycloak/services/migration/DefaultMigrationProvider.java
@@ -10,12 +10,14 @@ import org.keycloak.migration.MigrationProvider;
import org.keycloak.models.ClaimMask;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.services.managers.RealmManager;
/**
* Various common utils needed for migration from older version to newer
@@ -59,6 +61,11 @@ public class DefaultMigrationProvider implements MigrationProvider {
return providerFactory.getBuiltinMappers();
}
+ @Override
+ public void setupAdminCli(RealmModel realm) {
+ new RealmManager(session).setupAdminCli(realm);
+ }
+
@Override
public void close() {
}
diff --git a/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java b/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java
index 9fb60eca6d..194ad2d5a4 100755
--- a/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java
+++ b/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java
@@ -5,6 +5,7 @@ import java.util.List;
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
import org.keycloak.authentication.requiredactions.util.UserUpdateProfileContext;
+import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@@ -29,11 +30,12 @@ public class AttributeFormDataProcessor {
public static void process(MultivaluedMap formData, RealmModel realm, UpdateProfileContext user) {
for (String key : formData.keySet()) {
- if (!key.startsWith("user.attributes.")) continue;
- String attribute = key.substring("user.attributes.".length());
+ if (!key.startsWith(Constants.USER_ATTRIBUTES_PREFIX)) continue;
+ String attribute = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length());
// Need to handle case when attribute has multiple values, but in UI was displayed just first value
- List modelValue = new ArrayList<>(user.getAttribute(attribute));
+ List modelVal = user.getAttribute(attribute);
+ List modelValue = modelVal==null ? new ArrayList() : new ArrayList<>(modelVal);
int index = 0;
for (String value : formData.get(key)) {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 90eab4a54e..a4d5f27f77 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -34,6 +34,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.services.clientregistration.ClientRegistrationException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.LDAPConnectionTestManager;
import org.keycloak.services.managers.RealmManager;
diff --git a/services/src/test/java/org/keycloak/test/ResponseTypeTest.java b/services/src/test/java/org/keycloak/test/ResponseTypeTest.java
index b3f77a7b7f..dd15aa90e9 100644
--- a/services/src/test/java/org/keycloak/test/ResponseTypeTest.java
+++ b/services/src/test/java/org/keycloak/test/ResponseTypeTest.java
@@ -1,5 +1,8 @@
package org.keycloak.test;
+import java.util.Arrays;
+import java.util.Collections;
+
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
@@ -16,7 +19,7 @@ public class ResponseTypeTest {
assertFail("foo");
assertSuccess("code");
assertSuccess("none");
- assertFail("id_token");
+ assertSuccess("id_token");
assertFail("token");
assertFail("refresh_token");
assertSuccess("id_token token");
@@ -27,6 +30,38 @@ public class ResponseTypeTest {
assertFail("code refresh_token");
}
+ @Test
+ public void testMultipleResponseTypes() {
+ try {
+ OIDCResponseType.parse(Arrays.asList("code", "token"));
+ Assert.fail("Not expected to parse with success");
+ } catch (IllegalArgumentException iae) {
+ }
+
+ OIDCResponseType responseType = OIDCResponseType.parse(Collections.singletonList("code"));
+ Assert.assertTrue(responseType.hasResponseType("code"));
+ Assert.assertFalse(responseType.hasResponseType("none"));
+ Assert.assertFalse(responseType.isImplicitOrHybridFlow());
+
+ responseType = OIDCResponseType.parse(Arrays.asList("code", "none"));
+ Assert.assertTrue(responseType.hasResponseType("code"));
+ Assert.assertTrue(responseType.hasResponseType("none"));
+ Assert.assertFalse(responseType.isImplicitOrHybridFlow());
+
+ responseType = OIDCResponseType.parse(Arrays.asList("code", "code token"));
+ Assert.assertTrue(responseType.hasResponseType("code"));
+ Assert.assertFalse(responseType.hasResponseType("none"));
+ Assert.assertTrue(responseType.hasResponseType("token"));
+ Assert.assertFalse(responseType.hasResponseType("id_token"));
+ Assert.assertTrue(responseType.isImplicitOrHybridFlow());
+ Assert.assertFalse(responseType.isImplicitFlow());
+
+ responseType = OIDCResponseType.parse(Arrays.asList("id_token", "id_token token"));
+ Assert.assertFalse(responseType.hasResponseType("code"));
+ Assert.assertTrue(responseType.isImplicitOrHybridFlow());
+ Assert.assertTrue(responseType.isImplicitFlow());
+ }
+
private void assertSuccess(String responseType) {
OIDCResponseType.parse(responseType);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainersTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainersTestEnricher.java
index 9b68c4322e..f1328a90e3 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainersTestEnricher.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainersTestEnricher.java
@@ -183,7 +183,7 @@ public class ContainersTestEnricher {
private void initializeAdminClient() {
adminClient.set(Keycloak.getInstance(
getAuthServerContextRootFromSystemProperty() + "/auth",
- MASTER, ADMIN, ADMIN, Constants.ADMIN_CONSOLE_CLIENT_ID));
+ MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID));
}
private void initializeOAuthClient() {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientRegistrationTest.java
index 627865353f..37b53c2787 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientRegistrationTest.java
@@ -104,7 +104,7 @@ public abstract class AbstractClientRegistrationTest extends AbstractKeycloakTes
}
private String getToken(String username, String password) {
- return oauthClient.getToken(REALM_NAME, "security-admin-console", null, username, password).getToken();
+ return oauthClient.getToken(REALM_NAME, Constants.ADMIN_CLI_CLIENT_ID, null, username, password).getToken();
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
index 9696274241..f0fbe2bedc 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
@@ -2,12 +2,16 @@ package org.keycloak.testsuite.client;
import org.junit.Before;
import org.junit.Test;
+import org.keycloak.OAuth2Constants;
import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistrationException;
+import org.keycloak.common.util.CollectionUtil;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
+import java.util.Arrays;
import java.util.Collections;
import static org.junit.Assert.*;
@@ -49,6 +53,8 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
assertEquals("http://root", response.getClientUri());
assertEquals(1, response.getRedirectUris().size());
assertEquals("http://redirect", response.getRedirectUris().get(0));
+ assertEquals(Arrays.asList("code", "none"), response.getResponseTypes());
+ assertEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes());
}
@Test
@@ -59,6 +65,8 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
OIDCClientRepresentation rep = reg.oidc().get(response.getClientId());
assertNotNull(rep);
assertNotEquals(response.getRegistrationAccessToken(), rep.getRegistrationAccessToken());
+ assertTrue(CollectionUtil.collectionEquals(Arrays.asList("code", "none"), response.getResponseTypes()));
+ assertTrue(CollectionUtil.collectionEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes()));
}
@Test
@@ -67,11 +75,26 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
reg.auth(Auth.token(response));
response.setRedirectUris(Collections.singletonList("http://newredirect"));
+ response.setResponseTypes(Arrays.asList("code", "id_token token", "code id_token token"));
+ response.setGrantTypes(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD));
OIDCClientRepresentation updated = reg.oidc().update(response);
- assertEquals(1, updated.getRedirectUris().size());
- assertEquals("http://newredirect", updated.getRedirectUris().get(0));
+ assertTrue(CollectionUtil.collectionEquals(Collections.singletonList("http://newredirect"), updated.getRedirectUris()));
+ assertTrue(CollectionUtil.collectionEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD), updated.getGrantTypes()));
+ assertTrue(CollectionUtil.collectionEquals(Arrays.asList(OAuth2Constants.CODE, OIDCResponseType.NONE, OIDCResponseType.ID_TOKEN, "id_token token", "code id_token", "code token", "code id_token token"), updated.getResponseTypes()));
+ }
+
+ @Test
+ public void updateClientError() throws ClientRegistrationException {
+ try {
+ OIDCClientRepresentation response = create();
+ reg.auth(Auth.token(response));
+ response.setResponseTypes(Arrays.asList("code", "token"));
+ reg.oidc().update(response);
+ fail("Not expected to end with success");
+ } catch (ClientRegistrationException cre) {
+ }
}
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
index 84e724b99e..517f6c3d96 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
@@ -135,7 +135,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
return expect(EventType.CLIENT_LOGIN)
.detail(Details.CODE_ID, isCodeId())
.detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
- .detail(Details.RESPONSE_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)
+ .detail(Details.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)
.removeDetail(Details.CODE_ID)
.session(isUUID());
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
index 31b05508d8..1e6009674a 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
@@ -187,7 +187,7 @@ public class AdapterTestStrategy extends ExternalResource {
Assert.assertTrue(pageSource.contains("iPhone") && pageSource.contains("iPad"));
// View stats
- List