KEYCLOAK-2152 KEYCLOAK-2061 Client switches changes. Support for response_types and grant_types in OIDC Client registration

This commit is contained in:
mposolda 2015-11-30 12:40:04 +01:00
parent 0492d84b59
commit ec327c99f4
28 changed files with 241 additions and 61 deletions

View file

@ -23,4 +23,19 @@ public class CollectionUtil {
} }
return sb.toString(); return sb.toString();
} }
// Return true if all items from col1 are in col2 and viceversa. Order is not taken into account
public static <T> boolean collectionEquals(Collection<T> col1, Collection<T> col2) {
if (col1.size() != col2.size()) {
return false;
}
for (T item : col1) {
if (!col2.contains(item)) {
return false;
}
}
return true;
}
} }

View file

@ -83,7 +83,7 @@
<column name="IMPLICIT_FLOW_ENABLED" type="BOOLEAN" defaultValueBoolean="false"> <column name="IMPLICIT_FLOW_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
<column name="DIRECT_ACCESS_GRANTS_ENABLED" type="BOOLEAN" defaultValueBoolean="true"> <column name="DIRECT_ACCESS_GRANTS_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
</addColumn> </addColumn>

View file

@ -27,6 +27,8 @@ public interface OAuth2Constants {
String AUTHORIZATION_CODE = "authorization_code"; String AUTHORIZATION_CODE = "authorization_code";
String IMPLICIT = "implicit";
String PASSWORD = "password"; String PASSWORD = "password";
String CLIENT_CREDENTIALS = "client_credentials"; String CLIENT_CREDENTIALS = "client_credentials";

View file

@ -30,6 +30,7 @@ public class ClientRepresentation {
protected Boolean implicitFlowEnabled; protected Boolean implicitFlowEnabled;
protected Boolean directAccessGrantsEnabled; protected Boolean directAccessGrantsEnabled;
protected Boolean serviceAccountsEnabled; protected Boolean serviceAccountsEnabled;
@Deprecated
protected Boolean directGrantsOnly; protected Boolean directGrantsOnly;
protected Boolean publicClient; protected Boolean publicClient;
protected Boolean frontchannelLogout; protected Boolean frontchannelLogout;
@ -216,6 +217,7 @@ public class ClientRepresentation {
this.serviceAccountsEnabled = serviceAccountsEnabled; this.serviceAccountsEnabled = serviceAccountsEnabled;
} }
@Deprecated
public Boolean isDirectGrantsOnly() { public Boolean isDirectGrantsOnly() {
return directGrantsOnly; return directGrantsOnly;
} }

View file

@ -14,9 +14,9 @@ public class OIDCClientRepresentation {
private String token_endpoint_auth_method; private String token_endpoint_auth_method;
private String grant_types; private List<String> grant_types;
private String response_types; private List<String> response_types;
private String client_id; private String client_id;
@ -68,19 +68,19 @@ public class OIDCClientRepresentation {
this.token_endpoint_auth_method = token_endpoint_auth_method; this.token_endpoint_auth_method = token_endpoint_auth_method;
} }
public String getGrantTypes() { public List<String> getGrantTypes() {
return grant_types; return grant_types;
} }
public void setGrantTypes(String grantTypes) { public void setGrantTypes(List<String> grantTypes) {
this.grant_types = grantTypes; this.grant_types = grantTypes;
} }
public String getResponseTypes() { public List<String> getResponseTypes() {
return response_types; return response_types;
} }
public void setResponseTypes(String responseTypes) { public void setResponseTypes(List<String> responseTypes) {
this.response_types = responseTypes; this.response_types = responseTypes;
} }

View file

@ -12,6 +12,7 @@ public interface Details {
String REDIRECT_URI = "redirect_uri"; String REDIRECT_URI = "redirect_uri";
String RESPONSE_TYPE = "response_type"; String RESPONSE_TYPE = "response_type";
String RESPONSE_MODE = "response_mode"; String RESPONSE_MODE = "response_mode";
String GRANT_TYPE = "grant_type";
String AUTH_TYPE = "auth_type"; String AUTH_TYPE = "auth_type";
String AUTH_METHOD = "auth_method"; String AUTH_METHOD = "auth_method";
String IDENTITY_PROVIDER = "identity_provider"; String IDENTITY_PROVIDER = "identity_provider";

View file

@ -865,7 +865,6 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, $route, se
$scope.client = { $scope.client = {
enabled: true, enabled: true,
standardFlowEnabled: true, standardFlowEnabled: true,
directAccessGrantsEnabled: true,
attributes: {} attributes: {}
}; };
$scope.client.attributes['saml_signature_canonicalization_method'] = $scope.canonicalization[0].value; $scope.client.attributes['saml_signature_canonicalization_method'] = $scope.canonicalization[0].value;

View file

@ -34,7 +34,6 @@ public class ClientEntity extends AbstractIdentifiableEntity {
private boolean implicitFlowEnabled; private boolean implicitFlowEnabled;
private boolean directAccessGrantsEnabled; private boolean directAccessGrantsEnabled;
private boolean serviceAccountsEnabled; private boolean serviceAccountsEnabled;
private boolean directGrantsOnly;
private int nodeReRegistrationTimeout; private int nodeReRegistrationTimeout;
// We are using names of defaultRoles (not ids) // We are using names of defaultRoles (not ids)
@ -278,14 +277,6 @@ public class ClientEntity extends AbstractIdentifiableEntity {
this.serviceAccountsEnabled = serviceAccountsEnabled; this.serviceAccountsEnabled = serviceAccountsEnabled;
} }
public boolean isDirectGrantsOnly() {
return directGrantsOnly;
}
public void setDirectGrantsOnly(boolean directGrantsOnly) {
this.directGrantsOnly = directGrantsOnly;
}
public List<String> getDefaultRoles() { public List<String> getDefaultRoles() {
return defaultRoles; return defaultRoles;
} }

View file

@ -776,17 +776,19 @@ public class RepresentationToModel {
if (resourceRep.getBaseUrl() != null) client.setBaseUrl(resourceRep.getBaseUrl()); if (resourceRep.getBaseUrl() != null) client.setBaseUrl(resourceRep.getBaseUrl());
if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly()); if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly());
if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired()); 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 // Backwards compatibility only
if (resourceRep.isDirectGrantsOnly() != null) { if (resourceRep.isDirectGrantsOnly() != null) {
logger.warn("Using deprecated 'directGrantsOnly' configuration in JSON representation. It will be removed in future versions"); logger.warn("Using deprecated 'directGrantsOnly' configuration in JSON representation. It will be removed in future versions");
client.setStandardFlowEnabled(!resourceRep.isDirectGrantsOnly()); 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.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient());
if (resourceRep.isFrontchannelLogout() != null) client.setFrontchannelLogout(resourceRep.isFrontchannelLogout()); if (resourceRep.isFrontchannelLogout() != null) client.setFrontchannelLogout(resourceRep.isFrontchannelLogout());
if (resourceRep.getProtocol() != null) client.setProtocol(resourceRep.getProtocol()); if (resourceRep.getProtocol() != null) client.setProtocol(resourceRep.getProtocol());

View file

@ -726,7 +726,6 @@ public class RealmAdapter implements RealmModel {
entity.setClientId(clientId); entity.setClientId(clientId);
entity.setEnabled(true); entity.setEnabled(true);
entity.setStandardFlowEnabled(true); entity.setStandardFlowEnabled(true);
entity.setDirectAccessGrantsEnabled(true);
entity.setRealm(realm); entity.setRealm(realm);
realm.getClients().add(entity); realm.getClients().add(entity);
em.persist(entity); em.persist(entity);

View file

@ -811,7 +811,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
clientEntity.setRealmId(getId()); clientEntity.setRealmId(getId());
clientEntity.setEnabled(true); clientEntity.setEnabled(true);
clientEntity.setStandardFlowEnabled(true); clientEntity.setStandardFlowEnabled(true);
clientEntity.setDirectAccessGrantsEnabled(true);
getMongoStore().insertEntity(clientEntity, invocationContext); getMongoStore().insertEntity(clientEntity, invocationContext);
final ClientModel model = new ClientAdapter(session, this, clientEntity, invocationContext); final ClientModel model = new ClientAdapter(session, this, clientEntity, invocationContext);

View file

@ -5,6 +5,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter; import org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider; import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
import org.keycloak.services.clientregistration.ClientRegistrationException;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.POST; import javax.ws.rs.POST;

View file

@ -4,6 +4,7 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; 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.ClientRegistrationService;
import org.keycloak.services.clientregistration.oidc.OIDCClientRegistrationProviderFactory; import org.keycloak.services.clientregistration.oidc.OIDCClientRegistrationProviderFactory;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
@ -22,13 +23,13 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
public static final List<String> DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list("RS256"); public static final List<String> DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list("RS256");
public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS); public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS);
public static final List<String> DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE); public static final List<String> 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<String> DEFAULT_SUBJECT_TYPES_SUPPORTED = list("public"); public static final List<String> DEFAULT_SUBJECT_TYPES_SUPPORTED = list("public");
public static final List<String> DEFAULT_RESPONSE_MODES_SUPPORTED = list("query"); public static final List<String> DEFAULT_RESPONSE_MODES_SUPPORTED = list("query", "fragment", "form_post");
private KeycloakSession session; private KeycloakSession session;

View file

@ -178,6 +178,8 @@ public class TokenEndpoint {
} else { } else {
throw new ErrorResponseException(Errors.INVALID_REQUEST, "Invalid " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST); throw new ErrorResponseException(Errors.INVALID_REQUEST, "Invalid " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST);
} }
event.detail(Details.GRANT_TYPE, grantType);
} }
public Response buildAuthorizationCodeAccessTokenResponse() { public Response buildAuthorizationCodeAccessTokenResponse() {
@ -327,7 +329,7 @@ public class TokenEndpoint {
} }
public Response buildResourceOwnerPasswordCredentialsGrant() { 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.isConsentRequired()) { if (client.isConsentRequired()) {
event.error(Errors.CONSENT_DENIED); event.error(Errors.CONSENT_DENIED);
@ -393,8 +395,6 @@ public class TokenEndpoint {
throw new ErrorResponseException("unauthorized_client", "Client not enabled to retrieve service account", Response.Status.UNAUTHORIZED); 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); UserModel clientUser = session.users().getUserByServiceAccountClient(client);
if (clientUser == null || client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) { if (clientUser == null || client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) {

View file

@ -42,7 +42,7 @@ public abstract class OIDCRedirectUriBuilder {
// http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes // 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) { protected QueryRedirectUriBuilder(KeycloakUriBuilder uriBuilder) {
super(uriBuilder); super(uriBuilder);
@ -64,7 +64,7 @@ public abstract class OIDCRedirectUriBuilder {
// http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes // 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; private StringBuilder fragment;
@ -98,7 +98,7 @@ public abstract class OIDCRedirectUriBuilder {
// http://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html // 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<String, String> params = new HashMap<>(); private Map<String, String> params = new HashMap<>();

View file

@ -46,6 +46,16 @@ public class OIDCResponseType {
return new OIDCResponseType(allowedTypes); return new OIDCResponseType(allowedTypes);
} }
public static OIDCResponseType parse(List<String> responseTypes) {
OIDCResponseType result = new OIDCResponseType(new ArrayList<String>());
for (String respType : responseTypes) {
OIDCResponseType responseType = parse(respType);
result.responseTypes.addAll(responseType.responseTypes);
}
return result;
}
private static void validateAllowedTypes(List<String> responseTypes) { private static void validateAllowedTypes(List<String> responseTypes) {
if (responseTypes.size() == 0) { if (responseTypes.size() == 0) {
throw new IllegalStateException("No responseType provided"); throw new IllegalStateException("No responseType provided");
@ -53,9 +63,6 @@ public class OIDCResponseType {
if (responseTypes.contains(NONE) && responseTypes.size() > 1) { if (responseTypes.contains(NONE) && responseTypes.size() > 1) {
throw new IllegalArgumentException("None not allowed with some other response_type"); 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) { if (responseTypes.contains(TOKEN) && responseTypes.size() == 1) {
throw new IllegalArgumentException("Not supported to use response_type=token alone"); throw new IllegalArgumentException("Not supported to use response_type=token alone");
} }
@ -72,7 +79,7 @@ public class OIDCResponseType {
} }
public boolean isImplicitFlow() { public boolean isImplicitFlow() {
return hasResponseType(TOKEN) && hasResponseType(ID_TOKEN) && !hasResponseType(CODE); return hasResponseType(ID_TOKEN) && !hasResponseType(CODE);
} }

View file

@ -0,0 +1,23 @@
package org.keycloak.services.clientregistration;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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);
}
}

View file

@ -1,21 +1,48 @@
package org.keycloak.services.clientregistration.oidc; 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.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.services.clientregistration.ClientRegistrationException;
import java.net.URI; import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class DescriptionConverter { public class DescriptionConverter {
public static ClientRepresentation toInternal(OIDCClientRepresentation clientOIDC) { public static ClientRepresentation toInternal(OIDCClientRepresentation clientOIDC) throws ClientRegistrationException {
ClientRepresentation client = new ClientRepresentation(); ClientRepresentation client = new ClientRepresentation();
client.setClientId(clientOIDC.getClientId()); client.setClientId(clientOIDC.getClientId());
client.setName(clientOIDC.getClientName()); client.setName(clientOIDC.getClientName());
client.setRedirectUris(clientOIDC.getRedirectUris()); client.setRedirectUris(clientOIDC.getRedirectUris());
client.setBaseUrl(clientOIDC.getClientUri()); client.setBaseUrl(clientOIDC.getClientUri());
List<String> oidcResponseTypes = clientOIDC.getResponseTypes();
if (oidcResponseTypes == null || oidcResponseTypes.isEmpty()) {
oidcResponseTypes = Collections.singletonList(OIDCResponseType.CODE);
}
List<String> 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; return client;
} }
@ -28,7 +55,45 @@ public class DescriptionConverter {
response.setRedirectUris(client.getRedirectUris()); response.setRedirectUris(client.getRedirectUris());
response.setRegistrationAccessToken(client.getRegistrationAccessToken()); response.setRegistrationAccessToken(client.getRegistrationAccessToken());
response.setRegistrationClientUri(uri.toString()); response.setRegistrationClientUri(uri.toString());
response.setResponseTypes(getOIDCResponseTypes(client));
response.setGrantTypes(getOIDCGrantTypes(client));
return response; return response;
} }
private static List<String> getOIDCResponseTypes(ClientRepresentation client) {
List<String> 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<String> getOIDCGrantTypes(ClientRepresentation client) {
List<String> 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;
}
} }

View file

@ -1,5 +1,6 @@
package org.keycloak.services.clientregistration.oidc; package org.keycloak.services.clientregistration.oidc;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -9,6 +10,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider; import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
import org.keycloak.services.clientregistration.ClientRegistrationAuth; import org.keycloak.services.clientregistration.ClientRegistrationAuth;
import org.keycloak.services.clientregistration.ClientRegistrationException;
import org.keycloak.services.clientregistration.ErrorCodes; import org.keycloak.services.clientregistration.ErrorCodes;
import javax.ws.rs.*; import javax.ws.rs.*;
@ -21,6 +23,8 @@ import java.net.URI;
*/ */
public class OIDCClientRegistrationProvider extends AbstractClientRegistrationProvider { public class OIDCClientRegistrationProvider extends AbstractClientRegistrationProvider {
private static final Logger log = Logger.getLogger(OIDCClientRegistrationProvider.class);
public OIDCClientRegistrationProvider(KeycloakSession session) { public OIDCClientRegistrationProvider(KeycloakSession session) {
super(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); throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client Identifier included", Response.Status.BAD_REQUEST);
} }
try {
ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC); ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
client = create(client); client = create(client);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build(); URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
clientOIDC = DescriptionConverter.toExternalResponse(client, uri); clientOIDC = DescriptionConverter.toExternalResponse(client, uri);
clientOIDC.setClientIdIssuedAt(Time.currentTime()); clientOIDC.setClientIdIssuedAt(Time.currentTime());
return Response.created(uri).entity(clientOIDC).build(); 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 @GET
@ -54,11 +63,16 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
@Path("{clientId}") @Path("{clientId}")
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public Response updateOIDC(@PathParam("clientId") String clientId, OIDCClientRepresentation clientOIDC) { public Response updateOIDC(@PathParam("clientId") String clientId, OIDCClientRepresentation clientOIDC) {
try {
ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC); ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
client = update(clientId, client); client = update(clientId, client);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build(); URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
clientOIDC = DescriptionConverter.toExternalResponse(client, uri); clientOIDC = DescriptionConverter.toExternalResponse(client, uri);
return Response.ok(clientOIDC).build(); 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 @DELETE

View file

@ -34,6 +34,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.clientregistration.ClientRegistrationException;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.LDAPConnectionTestManager; import org.keycloak.services.managers.LDAPConnectionTestManager;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;

View file

@ -1,5 +1,8 @@
package org.keycloak.test; package org.keycloak.test;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.protocol.oidc.utils.OIDCResponseType;
@ -16,7 +19,7 @@ public class ResponseTypeTest {
assertFail("foo"); assertFail("foo");
assertSuccess("code"); assertSuccess("code");
assertSuccess("none"); assertSuccess("none");
assertFail("id_token"); assertSuccess("id_token");
assertFail("token"); assertFail("token");
assertFail("refresh_token"); assertFail("refresh_token");
assertSuccess("id_token token"); assertSuccess("id_token token");
@ -27,6 +30,38 @@ public class ResponseTypeTest {
assertFail("code refresh_token"); 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) { private void assertSuccess(String responseType) {
OIDCResponseType.parse(responseType); OIDCResponseType.parse(responseType);
} }

View file

@ -2,12 +2,16 @@ package org.keycloak.testsuite.client;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.client.registration.Auth; import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistrationException; 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.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation; import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -49,6 +53,8 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
assertEquals("http://root", response.getClientUri()); assertEquals("http://root", response.getClientUri());
assertEquals(1, response.getRedirectUris().size()); assertEquals(1, response.getRedirectUris().size());
assertEquals("http://redirect", response.getRedirectUris().get(0)); 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 @Test
@ -59,6 +65,8 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
OIDCClientRepresentation rep = reg.oidc().get(response.getClientId()); OIDCClientRepresentation rep = reg.oidc().get(response.getClientId());
assertNotNull(rep); assertNotNull(rep);
assertNotEquals(response.getRegistrationAccessToken(), rep.getRegistrationAccessToken()); 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 @Test
@ -67,11 +75,26 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
reg.auth(Auth.token(response)); reg.auth(Auth.token(response));
response.setRedirectUris(Collections.singletonList("http://newredirect")); 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); OIDCClientRepresentation updated = reg.oidc().update(response);
assertEquals(1, updated.getRedirectUris().size()); assertTrue(CollectionUtil.collectionEquals(Collections.singletonList("http://newredirect"), updated.getRedirectUris()));
assertEquals("http://newredirect", updated.getRedirectUris().get(0)); 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 @Test

View file

@ -135,7 +135,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
return expect(EventType.CLIENT_LOGIN) return expect(EventType.CLIENT_LOGIN)
.detail(Details.CODE_ID, isCodeId()) .detail(Details.CODE_ID, isCodeId())
.detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID) .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) .removeDetail(Details.CODE_ID)
.session(isUUID()); .session(isUUID());
} }

View file

@ -218,7 +218,7 @@ public class CustomFlowTest {
.client(clientId) .client(clientId)
.user(userId) .user(userId)
.session(accessToken.getSessionState()) .session(accessToken.getSessionState())
.detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD) .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, accessToken.getId()) .detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()) .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, login) .detail(Details.USERNAME, login)

View file

@ -257,7 +257,7 @@ public class GroupTest {
.client(clientId) .client(clientId)
.user(userId) .user(userId)
.session(accessToken.getSessionState()) .session(accessToken.getSessionState())
.detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD) .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, accessToken.getId()) .detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()) .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, login) .detail(Details.USERNAME, login)

View file

@ -189,7 +189,7 @@ public class ClientAuthSignedJWTTest {
events.expectLogin() events.expectLogin()
.client("client2") .client("client2")
.session(accessToken.getSessionState()) .session(accessToken.getSessionState())
.detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD) .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, accessToken.getId()) .detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()) .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, "test-user@localhost") .detail(Details.USERNAME, "test-user@localhost")

View file

@ -319,7 +319,7 @@ public class OfflineTokenTest {
.client("offline-client") .client("offline-client")
.user(userId) .user(userId)
.session(token.getSessionState()) .session(token.getSessionState())
.detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD) .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, token.getId()) .detail(Details.TOKEN_ID, token.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId()) .detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE) .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
@ -361,7 +361,7 @@ public class OfflineTokenTest {
.client("offline-client") .client("offline-client")
.user(userId) .user(userId)
.session(token.getSessionState()) .session(token.getSessionState())
.detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD) .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, token.getId()) .detail(Details.TOKEN_ID, token.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId()) .detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE) .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)

View file

@ -94,7 +94,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
.client(clientId) .client(clientId)
.user(userId) .user(userId)
.session(accessToken.getSessionState()) .session(accessToken.getSessionState())
.detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD) .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, accessToken.getId()) .detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()) .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, login) .detail(Details.USERNAME, login)
@ -130,7 +130,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
events.expectLogin() events.expectLogin()
.client("resource-owner") .client("resource-owner")
.session(accessToken.getSessionState()) .session(accessToken.getSessionState())
.detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD) .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, accessToken.getId()) .detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()) .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.removeDetail(Details.CODE_ID) .removeDetail(Details.CODE_ID)
@ -286,7 +286,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
events.expectLogin() events.expectLogin()
.client("resource-owner") .client("resource-owner")
.session((String) null) .session((String) null)
.detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD) .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
.removeDetail(Details.CODE_ID) .removeDetail(Details.CODE_ID)
.removeDetail(Details.REDIRECT_URI) .removeDetail(Details.REDIRECT_URI)
.removeDetail(Details.CONSENT) .removeDetail(Details.CONSENT)
@ -308,7 +308,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
.client("resource-owner") .client("resource-owner")
.user((String) null) .user((String) null)
.session((String) null) .session((String) null)
.detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD) .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.USERNAME, "invalid") .detail(Details.USERNAME, "invalid")
.removeDetail(Details.CODE_ID) .removeDetail(Details.CODE_ID)
.removeDetail(Details.REDIRECT_URI) .removeDetail(Details.REDIRECT_URI)