keycloak-uncached
Changes
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java 13(+7 -6)
integration/client-registration/src/main/java/org/keycloak/client/registration/HttpErrorException.java 23(+21 -2)
integration/client-registration/src/main/java/org/keycloak/client/registration/HttpUtil.java 34(+16 -18)
services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java 13(+3 -10)
services/src/main/java/org/keycloak/authentication/authenticators/directgrant/AbstractDirectGrantAuthenticator.java 9(+3 -6)
services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java 3(+2 -1)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java 47(+26 -21)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java 47(+39 -8)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationUriUtils.java 7(+7 -0)
services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java 2(+2 -0)
Details
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java
index ddd1152..78d98e7 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java
@@ -43,6 +43,7 @@ import org.keycloak.common.VerificationException;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.util.JsonSerialization;
import org.keycloak.common.util.KeycloakUriBuilder;
@@ -102,9 +103,9 @@ public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule {
StringBuilder errorBuilder = new StringBuilder("Login failed. Invalid status: " + status);
if (entity != null) {
InputStream is = entity.getContent();
- Map<String, String> errors = (Map<String, String>) JsonSerialization.readValue(is, Map.class);
- errorBuilder.append(", OAuth2 error. Error: " + errors.get(OAuth2Constants.ERROR))
- .append(", Error description: " + errors.get(OAuth2Constants.ERROR_DESCRIPTION));
+ OAuth2ErrorRepresentation errorRep = JsonSerialization.readValue(is, OAuth2ErrorRepresentation.class);
+ errorBuilder.append(", OAuth2 error. Error: " + errorRep.getError())
+ .append(", Error description: " + errorRep.getErrorDescription());
}
String error = errorBuilder.toString();
log.warn(error);
@@ -161,9 +162,9 @@ public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule {
if (entity != null) {
InputStream is = entity.getContent();
if (status == 400) {
- Map<String, String> errors = (Map<String, String>) JsonSerialization.readValue(is, Map.class);
- errorBuilder.append(", OAuth2 error. Error: " + errors.get(OAuth2Constants.ERROR))
- .append(", Error description: " + errors.get(OAuth2Constants.ERROR_DESCRIPTION));
+ OAuth2ErrorRepresentation errorRep = JsonSerialization.readValue(is, OAuth2ErrorRepresentation.class);
+ errorBuilder.append(", OAuth2 error. Error: " + errorRep.getError())
+ .append(", Error description: " + errorRep.getErrorDescription());
} else {
if (is != null) is.close();
diff --git a/core/src/main/java/org/keycloak/OAuthErrorException.java b/core/src/main/java/org/keycloak/OAuthErrorException.java
index f0bb862..940d434 100755
--- a/core/src/main/java/org/keycloak/OAuthErrorException.java
+++ b/core/src/main/java/org/keycloak/OAuthErrorException.java
@@ -37,12 +37,18 @@ public class OAuthErrorException extends Exception {
public static final String REQUEST_NOT_SUPPORTED = "request_not_supported";
public static final String REQUEST_URI_NOT_SUPPORTED = "request_uri_not_supported";
+ // OAuth2 Bearer Token Usage
+ public static final String INVALID_TOKEN = "invalid_token";
+ public static final String INSUFFICIENT_SCOPE = "insufficient_scope";
+
+ // OIDC Dynamic Client Registration
+ public static final String INVALID_REDIRECT_URI = "invalid_redirect_uri";
+ public static final String INVALID_CLIENT_METADATA = "invalid_client_metadata";
+
// Others
public static final String INVALID_CLIENT = "invalid_client";
public static final String INVALID_GRANT = "invalid_grant";
public static final String UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type";
- public static final String INVALID_TOKEN = "invalid_token";
- public static final String INSUFFICIENT_SCOPE = "insufficient_scope";
public OAuthErrorException(String error, String description, String message, Throwable cause) {
super(message, cause);
diff --git a/core/src/main/java/org/keycloak/representations/idm/OAuth2ErrorRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OAuth2ErrorRepresentation.java
new file mode 100644
index 0000000..f988013
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/OAuth2ErrorRepresentation.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.representations.idm;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.keycloak.OAuth2Constants;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OAuth2ErrorRepresentation {
+
+ private String error;
+ private String errorDescription;
+
+ public OAuth2ErrorRepresentation() {
+ }
+
+ public OAuth2ErrorRepresentation(String error, String errorDescription) {
+ this.error = error;
+ this.errorDescription = errorDescription;
+ }
+
+ @JsonProperty(OAuth2Constants.ERROR)
+ public String getError() {
+ return error;
+ }
+
+ public void setError(String error) {
+ this.error = error;
+ }
+
+ @JsonProperty(OAuth2Constants.ERROR_DESCRIPTION)
+ public String getErrorDescription() {
+ return errorDescription;
+ }
+
+ public void setErrorDescription(String errorDescription) {
+ this.errorDescription = errorDescription;
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java b/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java
index fc2973d..26082db 100644
--- a/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java
@@ -27,14 +27,20 @@ import java.util.List;
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
public class OIDCClientRepresentation {
+ // OIDC Dynamic client registration properties
+
private List<String> redirect_uris;
private String token_endpoint_auth_method;
+ private String token_endpoint_auth_signing_alg;
+
private List<String> grant_types;
private List<String> response_types;
+ private String application_type;
+
private String client_id;
private String client_secret;
@@ -47,7 +53,7 @@ public class OIDCClientRepresentation {
private String scope;
- private String contacts;
+ private List<String> contacts;
private String tos_uri;
@@ -57,10 +63,47 @@ public class OIDCClientRepresentation {
private String jwks;
+ private String sector_identifier_uri;
+
+ private String subject_type;
+
+ private String id_token_signed_response_alg;
+
+ private String id_token_encrypted_response_alg;
+
+ private String id_token_encrypted_response_enc;
+
+ private String userinfo_signed_response_alg;
+
+ private String userinfo_encrypted_response_alg;
+
+ private String userinfo_encrypted_response_enc;
+
+ private String request_object_signing_alg;
+
+ private String request_object_encryption_alg;
+
+ private String request_object_encryption_enc;
+
+ private Integer default_max_age;
+
+ private Boolean require_auth_time;
+
+ private List<String> default_acr_values;
+
+ private String initiate_login_uri;
+
+ private List<String> request_uris;
+
+ // OIDC Session Management
+ private List<String> post_logout_redirect_uris;
+
+ // Not sure from which specs this comes
private String software_id;
private String software_version;
+ // OIDC Dynamic Client Registration Response
private Integer client_id_issued_at;
private Integer client_secret_expires_at;
@@ -85,6 +128,14 @@ public class OIDCClientRepresentation {
this.token_endpoint_auth_method = token_endpoint_auth_method;
}
+ public String getTokenEndpointAuthSigningAlg() {
+ return token_endpoint_auth_signing_alg;
+ }
+
+ public void setTokenEndpointAuthSigningAlg(String token_endpoint_auth_signing_alg) {
+ this.token_endpoint_auth_signing_alg = token_endpoint_auth_signing_alg;
+ }
+
public List<String> getGrantTypes() {
return grant_types;
}
@@ -101,6 +152,14 @@ public class OIDCClientRepresentation {
this.response_types = responseTypes;
}
+ public String getApplicationType() {
+ return application_type;
+ }
+
+ public void setApplicationType(String applicationType) {
+ this.application_type = applicationType;
+ }
+
public String getClientId() {
return client_id;
}
@@ -149,11 +208,11 @@ public class OIDCClientRepresentation {
this.scope = scope;
}
- public String getContacts() {
+ public List<String> getContacts() {
return contacts;
}
- public void setContacts(String contacts) {
+ public void setContacts(List<String> contacts) {
this.contacts = contacts;
}
@@ -189,6 +248,142 @@ public class OIDCClientRepresentation {
this.jwks = jwks;
}
+ public String getSectorIdentifierUri() {
+ return sector_identifier_uri;
+ }
+
+ public void setSectorIdentifierUri(String sectorIdentifierUri) {
+ this.sector_identifier_uri = sectorIdentifierUri;
+ }
+
+ public String getSubjectType() {
+ return subject_type;
+ }
+
+ public void setSubjectType(String subjectType) {
+ this.subject_type = subjectType;
+ }
+
+ public String getIdTokenSignedResponseAlg() {
+ return id_token_signed_response_alg;
+ }
+
+ public void setIdTokenSignedResponseAlg(String idTokenSignedResponseAlg) {
+ this.id_token_signed_response_alg = idTokenSignedResponseAlg;
+ }
+
+ public String getIdTokenEncryptedResponseAlg() {
+ return id_token_encrypted_response_alg;
+ }
+
+ public void setIdTokenEncryptedResponseAlg(String idTokenEncryptedResponseAlg) {
+ this.id_token_encrypted_response_alg = idTokenEncryptedResponseAlg;
+ }
+
+ public String getIdTokenEncryptedResponseEnc() {
+ return id_token_encrypted_response_enc;
+ }
+
+ public void setIdTokenEncryptedResponseEnc(String idTokenEncryptedResponseEnc) {
+ this.id_token_encrypted_response_enc = idTokenEncryptedResponseEnc;
+ }
+
+ public String getUserinfoSignedResponseAlg() {
+ return userinfo_signed_response_alg;
+ }
+
+ public void setUserinfoSignedResponseAlg(String userinfo_signed_response_alg) {
+ this.userinfo_signed_response_alg = userinfo_signed_response_alg;
+ }
+
+ public String getUserinfoEncryptedResponseAlg() {
+ return userinfo_encrypted_response_alg;
+ }
+
+ public void setUserinfoEncryptedResponseAlg(String userinfo_encrypted_response_alg) {
+ this.userinfo_encrypted_response_alg = userinfo_encrypted_response_alg;
+ }
+
+ public String getUserinfoEncryptedResponseEnc() {
+ return userinfo_encrypted_response_enc;
+ }
+
+ public void setUserinfoEncryptedResponseEnc(String userinfo_encrypted_response_enc) {
+ this.userinfo_encrypted_response_enc = userinfo_encrypted_response_enc;
+ }
+
+ public String getRequestObjectSigningAlg() {
+ return request_object_signing_alg;
+ }
+
+ public void setRequestObjectSigningAlg(String request_object_signing_alg) {
+ this.request_object_signing_alg = request_object_signing_alg;
+ }
+
+ public String getRequestObjectEncryptionAlg() {
+ return request_object_encryption_alg;
+ }
+
+ public void setRequestObjectEncryptionAlg(String request_object_encryption_alg) {
+ this.request_object_encryption_alg = request_object_encryption_alg;
+ }
+
+ public String getRequestObjectEncryptionEnc() {
+ return request_object_encryption_enc;
+ }
+
+ public void setRequestObjectEncryptionEnc(String request_object_encryption_enc) {
+ this.request_object_encryption_enc = request_object_encryption_enc;
+ }
+
+ public Integer getDefaultMaxAge() {
+ return default_max_age;
+ }
+
+ public void setDefaultMaxAge(Integer default_max_age) {
+ this.default_max_age = default_max_age;
+ }
+
+ public Boolean getRequireAuthTime() {
+ return require_auth_time;
+ }
+
+ public void setRequireAuthTime(Boolean require_auth_time) {
+ this.require_auth_time = require_auth_time;
+ }
+
+ public List<String> getDefaultAcrValues() {
+ return default_acr_values;
+ }
+
+ public void setDefaultAcrValues(List<String> default_acr_values) {
+ this.default_acr_values = default_acr_values;
+ }
+
+ public String getInitiateLoginUri() {
+ return initiate_login_uri;
+ }
+
+ public void setInitiateLoginUri(String initiate_login_uri) {
+ this.initiate_login_uri = initiate_login_uri;
+ }
+
+ public List<String> getRequestUris() {
+ return request_uris;
+ }
+
+ public void setRequestUris(List<String> requestUris) {
+ this.request_uris = requestUris;
+ }
+
+ public List<String> getPostLogoutRedirectUris() {
+ return post_logout_redirect_uris;
+ }
+
+ public void setPostLogoutRedirectUris(List<String> post_logout_redirect_uris) {
+ this.post_logout_redirect_uris = post_logout_redirect_uris;
+ }
+
public String getSoftwareId() {
return software_id;
}
diff --git a/core/src/test/java/org/keycloak/JsonParserTest.java b/core/src/test/java/org/keycloak/JsonParserTest.java
index 69416b7..062dc9b 100755
--- a/core/src/test/java/org/keycloak/JsonParserTest.java
+++ b/core/src/test/java/org/keycloak/JsonParserTest.java
@@ -33,6 +33,7 @@ import org.junit.Test;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.util.JsonSerialization;
/**
@@ -125,4 +126,15 @@ public class JsonParserTest {
System.out.println(sb.toString());
}
+ @Test
+ public void testReadOIDCClientRep() throws IOException {
+ String stringRep = "{\"subject_type\": \"public\", \"jwks_uri\": \"https://op.certification.openid.net:60720/export/jwk_60720.json\", \"contacts\": [\"roland.hedberg@umu.se\"], \"application_type\": \"web\", \"grant_types\": [\"authorization_code\"], \"post_logout_redirect_uris\": [\"https://op.certification.openid.net:60720/logout\"], \"redirect_uris\": [\"https://op.certification.openid.net:60720/authz_cb\"], \"response_types\": [\"code\"], \"require_auth_time\": true, \"default_max_age\": 3600}";
+ OIDCClientRepresentation clientRep = JsonSerialization.readValue(stringRep, OIDCClientRepresentation.class);
+ Assert.assertEquals("public", clientRep.getSubjectType());
+ Assert.assertTrue(clientRep.getRequireAuthTime());
+ Assert.assertEquals(3600, clientRep.getDefaultMaxAge().intValue());
+ Assert.assertEquals(1, clientRep.getRedirectUris().size());
+ Assert.assertEquals("https://op.certification.openid.net:60720/authz_cb", clientRep.getRedirectUris().get(0));
+ }
+
}
diff --git a/integration/client-registration/src/main/java/org/keycloak/client/registration/HttpErrorException.java b/integration/client-registration/src/main/java/org/keycloak/client/registration/HttpErrorException.java
index 4078b73..640bc0d 100644
--- a/integration/client-registration/src/main/java/org/keycloak/client/registration/HttpErrorException.java
+++ b/integration/client-registration/src/main/java/org/keycloak/client/registration/HttpErrorException.java
@@ -18,6 +18,8 @@
package org.keycloak.client.registration;
import org.apache.http.StatusLine;
+import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
+import org.keycloak.util.JsonSerialization;
import java.io.IOException;
@@ -26,14 +28,31 @@ import java.io.IOException;
*/
public class HttpErrorException extends IOException {
- private StatusLine statusLine;
+ private final StatusLine statusLine;
+ private final String errorResponse;
- public HttpErrorException(StatusLine statusLine) {
+ public HttpErrorException(StatusLine statusLine, String errorResponse) {
this.statusLine = statusLine;
+ this.errorResponse = errorResponse;
}
public StatusLine getStatusLine() {
return statusLine;
}
+ public String getErrorResponse() {
+ return errorResponse;
+ }
+
+ public OAuth2ErrorRepresentation toErrorRepresentation() {
+ if (errorResponse == null) {
+ return null;
+ }
+
+ try {
+ return JsonSerialization.readValue(errorResponse, OAuth2ErrorRepresentation.class);
+ } catch (IOException ioe) {
+ throw new RuntimeException("Not OAuth2 error");
+ }
+ }
}
diff --git a/integration/client-registration/src/main/java/org/keycloak/client/registration/HttpUtil.java b/integration/client-registration/src/main/java/org/keycloak/client/registration/HttpUtil.java
index 8d524ce..66808ed 100644
--- a/integration/client-registration/src/main/java/org/keycloak/client/registration/HttpUtil.java
+++ b/integration/client-registration/src/main/java/org/keycloak/client/registration/HttpUtil.java
@@ -23,9 +23,7 @@ import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.*;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
-import org.keycloak.client.registration.Auth;
-import org.keycloak.client.registration.ClientRegistrationException;
-import org.keycloak.client.registration.HttpErrorException;
+import org.keycloak.common.util.StreamUtil;
import java.io.IOException;
import java.io.InputStream;
@@ -69,8 +67,7 @@ class HttpUtil {
if (response.getStatusLine().getStatusCode() == 201) {
return responseStream;
} else {
- responseStream.close();
- throw new HttpErrorException(response.getStatusLine());
+ throw httpErrorException(response, responseStream);
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
@@ -97,10 +94,7 @@ class HttpUtil {
responseStream.close();
return null;
} else {
- if (responseStream != null) {
- responseStream.close();
- }
- throw new HttpErrorException(response.getStatusLine());
+ throw httpErrorException(response, responseStream);
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
@@ -118,9 +112,6 @@ class HttpUtil {
addAuth(request);
HttpResponse response = httpClient.execute(request);
- if (response.getEntity() != null) {
- response.getEntity().getContent();
- }
InputStream responseStream = null;
if (response.getEntity() != null) {
@@ -130,10 +121,7 @@ class HttpUtil {
if (response.getStatusLine().getStatusCode() == 200) {
return responseStream;
} else {
- if (responseStream != null) {
- responseStream.close();
- }
- throw new HttpErrorException(response.getStatusLine());
+ throw httpErrorException(response, responseStream);
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
@@ -147,12 +135,13 @@ class HttpUtil {
addAuth(request);
HttpResponse response = httpClient.execute(request);
+ InputStream responseStream = null;
if (response.getEntity() != null) {
- response.getEntity().getContent().close();
+ responseStream = response.getEntity().getContent();
}
if (response.getStatusLine().getStatusCode() != 204) {
- throw new HttpErrorException(response.getStatusLine());
+ throw httpErrorException(response, responseStream);
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
@@ -185,4 +174,13 @@ class HttpUtil {
}
}
+ private HttpErrorException httpErrorException(HttpResponse response, InputStream responseStream) throws IOException {
+ if (responseStream != null) {
+ String errorResponse = StreamUtil.readString(responseStream);
+ return new HttpErrorException(response.getStatusLine(), errorResponse);
+ } else {
+ return new HttpErrorException(response.getStatusLine(), null);
+ }
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java
index 0d7d911..94cefa3 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java
@@ -17,13 +17,10 @@
package org.keycloak.authentication.authenticators.client;
-import java.util.HashMap;
-import java.util.Map;
-
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
-import org.keycloak.OAuth2Constants;
+import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -32,12 +29,8 @@ public class ClientAuthUtil {
public static Response errorResponse(int status, String error, String errorDescription) {
- Map<String, String> e = new HashMap<String, String>();
- e.put(OAuth2Constants.ERROR, error);
- if (errorDescription != null) {
- e.put(OAuth2Constants.ERROR_DESCRIPTION, errorDescription);
- }
- return Response.status(status).entity(e).type(MediaType.APPLICATION_JSON_TYPE).build();
+ OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription);
+ return Response.status(status).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build();
}
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/AbstractDirectGrantAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/AbstractDirectGrantAuthenticator.java
index 8de4230..88d0e15 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/AbstractDirectGrantAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/AbstractDirectGrantAuthenticator.java
@@ -24,6 +24,7 @@ import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@@ -36,12 +37,8 @@ import java.util.Map;
*/
public abstract class AbstractDirectGrantAuthenticator implements Authenticator, AuthenticatorFactory {
public Response errorResponse(int status, String error, String errorDescription) {
- Map<String, String> e = new HashMap<String, String>();
- e.put(OAuth2Constants.ERROR, error);
- if (errorDescription != null) {
- e.put(OAuth2Constants.ERROR_DESCRIPTION, errorDescription);
- }
- return Response.status(status).entity(e).type(MediaType.APPLICATION_JSON_TYPE).build();
+ OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription);
+ return Response.status(status).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build();
}
@Override
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
index ffda262..87fab80 100755
--- a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
@@ -54,8 +54,9 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
client = ModelToRepresentation.toRepresentation(clientModel);
- String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, clientModel);
+ client.setSecret(clientModel.getSecret());
+ String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, clientModel);
client.setRegistrationAccessToken(registrationAccessToken);
if (auth.isInitialAccessToken()) {
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java
index 5564ef2..7886cf8 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java
@@ -21,8 +21,10 @@ import org.jboss.resteasy.spi.Failure;
import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.Config;
+import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.util.Time;
+import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AdminRoles;
@@ -33,7 +35,9 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.JsonWebToken;
+import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ForbiddenException;
+import org.keycloak.services.ServicesLogger;
import org.keycloak.util.TokenUtil;
import javax.ws.rs.core.HttpHeaders;
@@ -65,23 +69,24 @@ public class ClientRegistrationAuth {
String authorizationHeader = session.getContext().getRequestHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null) {
- return;
+ throw unauthorized("Missing Authorization header");
}
String[] split = authorizationHeader.split(" ");
if (!split[0].equalsIgnoreCase("bearer")) {
- return;
+ throw unauthorized("Invalid Authorization header. Expected type: Bearer");
}
- jwt = ClientRegistrationTokenUtils.verifyToken(realm, uri, split[1]);
- if (jwt == null) {
- throw unauthorized();
+ ClientRegistrationTokenUtils.TokenVerification tokenVerification = ClientRegistrationTokenUtils.verifyToken(realm, uri, split[1]);
+ if (tokenVerification.getError() != null) {
+ throw unauthorized(tokenVerification.getError().getMessage());
}
+ jwt = tokenVerification.getJwt();
if (isInitialAccessToken()) {
initialAccessModel = session.sessions().getClientInitialAccessModel(session.getContext().getRealm(), jwt.getId());
if (initialAccessModel == null) {
- throw unauthorized();
+ throw unauthorized("Initial Access Token not found");
}
}
}
@@ -115,7 +120,7 @@ public class ClientRegistrationAuth {
}
}
- throw unauthorized();
+ throw unauthorized("Not authorized to view client. Maybe bad token type.");
}
public void requireView(ClientModel client) {
@@ -131,18 +136,18 @@ public class ClientRegistrationAuth {
throw forbidden();
}
} else if (isRegistrationAccessToken()) {
- if (client.getRegistrationToken() != null && client != null && client.getRegistrationToken().equals(jwt.getId())) {
+ if (client.getRegistrationToken() != null && client.getRegistrationToken().equals(jwt.getId())) {
return;
}
} else if (isInitialAccessToken()) {
- throw unauthorized();
+ throw unauthorized("Not initial access token");
} else {
if (authenticateClient(client)) {
return;
}
}
- throw unauthorized();
+ throw unauthorized("Not authorized to view client. Maybe bad token type.");
}
public void requireUpdate(ClientModel client) {
@@ -163,7 +168,7 @@ public class ClientRegistrationAuth {
}
}
- throw unauthorized();
+ throw unauthorized("Not authorized to update client. Maybe bad token type.");
}
public ClientInitialAccessModel getInitialAccessModel() {
@@ -218,36 +223,36 @@ public class ClientRegistrationAuth {
Response response = processor.authenticateClient();
if (response != null) {
event.client(client.getClientId()).error(Errors.NOT_ALLOWED);
- throw unauthorized();
+ throw unauthorized("Failed to authenticate client");
}
ClientModel authClient = processor.getClient();
- if (client == null) {
+ if (authClient == null) {
event.client(client.getClientId()).error(Errors.NOT_ALLOWED);
- throw unauthorized();
+ throw unauthorized("No client authenticated");
}
if (!authClient.getClientId().equals(client.getClientId())) {
event.client(client.getClientId()).error(Errors.NOT_ALLOWED);
- throw unauthorized();
+ throw unauthorized("Different client authenticated");
}
return true;
}
- private Failure unauthorized() {
- event.error(Errors.NOT_ALLOWED);
- return new UnauthorizedException();
+ private Failure unauthorized(String errorDescription) {
+ event.detail(Details.REASON, errorDescription).error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, errorDescription, Response.Status.UNAUTHORIZED);
}
private Failure forbidden() {
event.error(Errors.NOT_ALLOWED);
- return new ForbiddenException();
+ throw new ErrorResponseException(OAuthErrorException.INSUFFICIENT_SCOPE, "Forbidden", Response.Status.FORBIDDEN);
}
private Failure notFound() {
- event.error(Errors.NOT_ALLOWED);
- return new NotFoundException("Client not found");
+ event.error(Errors.CLIENT_NOT_FOUND);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client not found", Response.Status.NOT_FOUND);
}
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
index 2fe65cc..2f33e5d 100755
--- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
@@ -56,40 +56,44 @@ public class ClientRegistrationTokenUtils {
return createToken(realm, uri, model.getId(), TYPE_INITIAL_ACCESS_TOKEN, model.getExpiration() > 0 ? model.getTimestamp() + model.getExpiration() : 0);
}
- public static JsonWebToken verifyToken(RealmModel realm, UriInfo uri, String token) {
+ public static TokenVerification verifyToken(RealmModel realm, UriInfo uri, String token) {
+ if (token == null) {
+ return TokenVerification.error(new RuntimeException("Missing token"));
+ }
+
JWSInput input;
try {
input = new JWSInput(token);
} catch (JWSInputException e) {
- return null;
+ return TokenVerification.error(new RuntimeException("Invalid token", e));
}
if (!RSAProvider.verify(input, realm.getPublicKey())) {
- return null;
+ return TokenVerification.error(new RuntimeException("Failed verify token"));
}
JsonWebToken jwt;
try {
jwt = input.readJsonContent(JsonWebToken.class);
} catch (JWSInputException e) {
- return null;
+ return TokenVerification.error(new RuntimeException("Token is not JWT", e));
}
if (!getIssuer(realm, uri).equals(jwt.getIssuer())) {
- return null;
+ return TokenVerification.error(new RuntimeException("Issuer from token don't match with the realm issuer."));
}
if (!jwt.isActive()) {
- return null;
+ return TokenVerification.error(new RuntimeException("Token not active."));
}
if (!(TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType()) ||
TYPE_INITIAL_ACCESS_TOKEN.equals(jwt.getType()) ||
TYPE_REGISTRATION_ACCESS_TOKEN.equals(jwt.getType()))) {
- return null;
+ return TokenVerification.error(new RuntimeException("Invalid type of token"));
}
- return jwt;
+ return TokenVerification.success(jwt);
}
private static String createToken(RealmModel realm, UriInfo uri, String id, String type, int expiration) {
@@ -112,4 +116,31 @@ public class ClientRegistrationTokenUtils {
return Urls.realmIssuer(uri.getBaseUri(), realm.getName());
}
+ protected static class TokenVerification {
+
+ private final JsonWebToken jwt;
+ private final RuntimeException error;
+
+ public static TokenVerification success(JsonWebToken jwt) {
+ return new TokenVerification(jwt, null);
+ }
+
+ public static TokenVerification error(RuntimeException error) {
+ return new TokenVerification(null, error);
+ }
+
+ private TokenVerification(JsonWebToken jwt, RuntimeException error) {
+ this.jwt = jwt;
+ this.error = error;
+ }
+
+ public JsonWebToken getJwt() {
+ return jwt;
+ }
+
+ public RuntimeException getError() {
+ return error;
+ }
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationUriUtils.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationUriUtils.java
new file mode 100644
index 0000000..3108003
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationUriUtils.java
@@ -0,0 +1,7 @@
+package org.keycloak.services.clientregistration;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientRegistrationUriUtils {
+}
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 cfbf42d..b797282 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
@@ -66,6 +66,8 @@ public class DescriptionConverter {
public static OIDCClientRepresentation toExternalResponse(ClientRepresentation client, URI uri) {
OIDCClientRepresentation response = new OIDCClientRepresentation();
response.setClientId(client.getClientId());
+ response.setClientSecret(client.getSecret());
+ response.setClientSecretExpiresAt(0);
response.setClientName(client.getName());
response.setClientUri(client.getBaseUrl());
response.setClientSecret(client.getSecret());
diff --git a/services/src/main/java/org/keycloak/services/ErrorResponseException.java b/services/src/main/java/org/keycloak/services/ErrorResponseException.java
index c29f0f3..538374c 100644
--- a/services/src/main/java/org/keycloak/services/ErrorResponseException.java
+++ b/services/src/main/java/org/keycloak/services/ErrorResponseException.java
@@ -18,6 +18,7 @@
package org.keycloak.services;
import org.keycloak.OAuth2Constants;
+import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
@@ -42,12 +43,8 @@ public class ErrorResponseException extends WebApplicationException {
@Override
public Response getResponse() {
- Map<String, String> e = new HashMap<String, String>();
- e.put(OAuth2Constants.ERROR, error);
- if (errorDescription != null) {
- e.put(OAuth2Constants.ERROR_DESCRIPTION, errorDescription);
- }
- return Response.status(status).entity(e).type(MediaType.APPLICATION_JSON_TYPE).build();
+ OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription);
+ return Response.status(status).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build();
}
}
diff --git a/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java
index c633587..898ad3c 100755
--- a/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java
+++ b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java
@@ -19,6 +19,7 @@ package org.keycloak.services.resources;
import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.UnauthorizedException;
+import org.keycloak.OAuthErrorException;
import org.keycloak.common.ClientConnection;
import org.keycloak.OAuth2Constants;
import org.keycloak.constants.AdapterConstants;
@@ -30,6 +31,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
+import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.services.ForbiddenException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.common.util.Time;
@@ -172,11 +174,9 @@ public class ClientsManagementService {
ClientModel client = AuthorizeClientUtil.authorizeClient(session, event).getClient();
if (client.isPublicClient()) {
- Map<String, String> error = new HashMap<String, String>();
- error.put(OAuth2Constants.ERROR, "invalid_client");
- error.put(OAuth2Constants.ERROR_DESCRIPTION, "Public clients not allowed");
+ OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(OAuthErrorException.INVALID_CLIENT, "Public clients not allowed");
event.error(Errors.INVALID_CLIENT);
- throw new BadRequestException("Public clients not allowed", javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build());
+ throw new BadRequestException("Public clients not allowed", javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build());
}
return client;
@@ -185,11 +185,9 @@ public class ClientsManagementService {
protected String getClientClusterHost(MultivaluedMap<String, String> formData) {
String clientClusterHost = formData.getFirst(AdapterConstants.CLIENT_CLUSTER_HOST);
if (clientClusterHost == null || clientClusterHost.length() == 0) {
- Map<String, String> error = new HashMap<String, String>();
- error.put(OAuth2Constants.ERROR, "invalid_request");
- error.put(OAuth2Constants.ERROR_DESCRIPTION, "Client cluster host not specified");
+ OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation( OAuthErrorException.INVALID_REQUEST, "Client cluster host not specified");
event.error(Errors.INVALID_CODE);
- throw new BadRequestException("Cluster host not specified", javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build());
+ throw new BadRequestException("Cluster host not specified", javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build());
}
return clientClusterHost;
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 ab587c3..9b364e8 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
@@ -20,13 +20,17 @@ package org.keycloak.testsuite.client;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistrationException;
+import org.keycloak.client.registration.HttpErrorException;
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.idm.OAuth2ErrorRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
+import org.keycloak.testsuite.Assert;
import java.util.Arrays;
import java.util.Collections;
@@ -58,13 +62,33 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
}
@Test
+ public void testMissingToken() throws Exception {
+ reg.auth(null);
+
+ OIDCClientRepresentation client = new OIDCClientRepresentation();
+ client.setClientName("RegistrationAccessTokenTest");
+ client.setClientUri("http://root");
+ client.setRedirectUris(Collections.singletonList("http://redirect"));
+
+ try {
+ reg.oidc().create(client);
+ Assert.fail("Not expected to successfuly register client");
+ } catch (ClientRegistrationException expected) {
+ HttpErrorException httpEx = (HttpErrorException) expected.getCause();
+ Assert.assertEquals(401, httpEx.getStatusLine().getStatusCode());
+ Assert.assertEquals(OAuthErrorException.INVALID_TOKEN, httpEx.toErrorRepresentation().getError());
+ }
+ }
+
+ @Test
public void createClient() throws ClientRegistrationException {
OIDCClientRepresentation response = create();
assertNotNull(response.getRegistrationAccessToken());
assertNotNull(response.getClientIdIssuedAt());
assertNotNull(response.getClientId());
- assertNull(response.getClientSecretExpiresAt());
+ assertNotNull(response.getClientSecret());
+ assertEquals(0, response.getClientSecretExpiresAt().intValue());
assertNotNull(response.getRegistrationClientUri());
assertEquals("RegistrationAccessTokenTest", response.getClientName());
assertEquals("http://root", response.getClientUri());
@@ -84,6 +108,8 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
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()));
+ assertNotNull(response.getClientSecret());
+ assertEquals(0, response.getClientSecretExpiresAt().intValue());
}
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
index 900767f..ec800ed 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
@@ -21,12 +21,15 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.oidc.TokenMetadataRepresentation;
+import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.TestRealmKeycloakTest;
import org.keycloak.testsuite.util.KeycloakModelUtils;
@@ -112,7 +115,9 @@ public class TokenIntrospectionTest extends TestRealmKeycloakTest {
AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password");
String tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "bad_credential", accessTokenResponse.getAccessToken());
- assertEquals("{\"error_description\":\"Authentication failed.\",\"error\":\"invalid_request\"}", tokenResponse);
+ OAuth2ErrorRepresentation errorRep = JsonSerialization.readValue(tokenResponse, OAuth2ErrorRepresentation.class);
+ Assert.assertEquals("Authentication failed.", errorRep.getErrorDescription());
+ Assert.assertEquals(OAuthErrorException.INVALID_REQUEST, errorRep.getError());
}
@Test
@@ -157,7 +162,9 @@ public class TokenIntrospectionTest extends TestRealmKeycloakTest {
AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password");
String tokenResponse = oauth.introspectAccessTokenWithClientCredential("public-cli", "it_doesnt_matter", accessTokenResponse.getAccessToken());
- assertEquals("{\"error_description\":\"Client not allowed.\",\"error\":\"invalid_request\"}", tokenResponse);
+ OAuth2ErrorRepresentation errorRep = JsonSerialization.readValue(tokenResponse, OAuth2ErrorRepresentation.class);
+ Assert.assertEquals("Client not allowed.", errorRep.getErrorDescription());
+ Assert.assertEquals(OAuthErrorException.INVALID_REQUEST, errorRep.getError());
}
@Test