keycloak-memoizeit
Changes
services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java 4(+4 -0)
services/src/main/java/org/keycloak/authentication/authenticators/client/X509ClientAuthenticator.java 4(+4 -0)
services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewEndpoint.java 174(+174 -0)
services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewEndpointFactory.java 46(+46 -0)
services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewRequestRepresentation.java 80(+80 -0)
services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewResponseRepresentation.java 154(+154 -0)
services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.ext.OIDCExtProviderFactory 1(+1 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/DummyClientAuthenticator.java 117(+117 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java 4(+3 -1)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory 3(+2 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java 14(+12 -2)
Details
diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java
index 88d4f70..19f57c1 100755
--- a/common/src/main/java/org/keycloak/common/Profile.java
+++ b/common/src/main/java/org/keycloak/common/Profile.java
@@ -33,7 +33,13 @@ import java.util.Set;
public class Profile {
public enum Feature {
- AUTHORIZATION, IMPERSONATION, SCRIPTS, DOCKER, ACCOUNT2, TOKEN_EXCHANGE
+ ACCOUNT2,
+ AUTHORIZATION,
+ DOCKER,
+ IMPERSONATION,
+ OPENSHIFT_INTEGRATION,
+ SCRIPTS,
+ TOKEN_EXCHANGE
}
private enum ProductValue {
diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java
index 72a0d4c..1b90176 100755
--- a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java
+++ b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java
@@ -76,6 +76,8 @@ public interface KeycloakSession {
Class<? extends Provider> getProviderClass(String providerClassName);
Object getAttribute(String attribute);
+ <T> T getAttribute(String attribute, Class<T> clazz);
+
Object removeAttribute(String attribute);
void setAttribute(String name, Object value);
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
index 78f58da..6e051df 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
@@ -89,6 +89,10 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
}
if (client_id == null) {
+ client_id = context.getSession().getAttribute("client_id", String.class);
+ }
+
+ if (client_id == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Missing client_id parameter");
context.challenge(challengeResponse);
return;
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/X509ClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/X509ClientAuthenticator.java
index f6b7b91..324f806 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/X509ClientAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/X509ClientAuthenticator.java
@@ -67,6 +67,10 @@ public class X509ClientAuthenticator extends AbstractClientAuthenticator {
}
if (client_id == null) {
+ client_id = context.getSession().getAttribute("client_id", String.class);
+ }
+
+ if (client_id == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Missing client_id parameter");
context.challenge(challengeResponse);
return;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/ext/OIDCExtProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/ext/OIDCExtProvider.java
new file mode 100644
index 0000000..6ce631f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/ext/OIDCExtProvider.java
@@ -0,0 +1,14 @@
+package org.keycloak.protocol.oidc.ext;
+
+import org.keycloak.events.EventBuilder;
+import org.keycloak.provider.Provider;
+
+public interface OIDCExtProvider extends Provider {
+
+ void setEvent(EventBuilder event);
+
+ @Override
+ default void close() {
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/ext/OIDCExtProviderFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/ext/OIDCExtProviderFactory.java
new file mode 100644
index 0000000..951a335
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/ext/OIDCExtProviderFactory.java
@@ -0,0 +1,29 @@
+package org.keycloak.protocol.oidc.ext;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.provider.ProviderFactory;
+
+public interface OIDCExtProviderFactory extends ProviderFactory<OIDCExtProvider> {
+
+ @Override
+ default void init(Config.Scope config) {
+
+ }
+
+ @Override
+ default void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ default void close() {
+
+ }
+
+ @Override
+ default int order() {
+ return 0;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/ext/OIDCExtSPI.java b/services/src/main/java/org/keycloak/protocol/oidc/ext/OIDCExtSPI.java
new file mode 100644
index 0000000..29821e5
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/ext/OIDCExtSPI.java
@@ -0,0 +1,29 @@
+package org.keycloak.protocol.oidc.ext;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+public class OIDCExtSPI implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "openid-connect-ext";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return OIDCExtProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return OIDCExtProviderFactory.class;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
index 8e2784f..3fe7268 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
@@ -35,6 +35,7 @@ import org.keycloak.protocol.oidc.endpoints.LoginStatusIframeEndpoint;
import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint;
+import org.keycloak.protocol.oidc.ext.OIDCExtProvider;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.Cors;
@@ -42,8 +43,10 @@ import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.CacheControlUtil;
import javax.ws.rs.GET;
+import javax.ws.rs.NotFoundException;
import javax.ws.rs.OPTIONS;
import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
@@ -258,4 +261,14 @@ public class OIDCLoginProtocolService {
}
}
+ @Path("ext/{extension}")
+ public Object resolveExtension(@PathParam("extension") String extension) {
+ OIDCExtProvider provider = session.getProvider(OIDCExtProvider.class, extension);
+ if (provider != null) {
+ provider.setEvent(event);
+ return provider;
+ }
+ throw new NotFoundException();
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewEndpoint.java b/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewEndpoint.java
new file mode 100644
index 0000000..100207b
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewEndpoint.java
@@ -0,0 +1,174 @@
+/*
+ * 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.protocol.openshift;
+
+import org.keycloak.RSATokenVerifier;
+import org.keycloak.common.VerificationException;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.protocol.oidc.ext.OIDCExtProvider;
+import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.Urls;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.security.PublicKey;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class OpenShiftTokenReviewEndpoint implements OIDCExtProvider {
+
+ private KeycloakSession session;
+ private TokenManager tokenManager;
+ private EventBuilder event;
+
+ public OpenShiftTokenReviewEndpoint(KeycloakSession session) {
+ this.session = session;
+ this.tokenManager = new TokenManager();
+ }
+
+ @Override
+ public void setEvent(EventBuilder event) {
+ this.event = event;
+ }
+
+ @Path("/")
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response tokenReview(OpenShiftTokenReviewRequestRepresentation reviewRequest) throws Exception {
+ return tokenReview(null, reviewRequest);
+ }
+
+ @Path("/{client_id}")
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response tokenReview(@PathParam("client_id") String clientId, OpenShiftTokenReviewRequestRepresentation reviewRequest) throws Exception {
+ event.event(EventType.INTROSPECT_TOKEN);
+
+ if (clientId != null) {
+ session.setAttribute("client_id", clientId);
+ }
+
+ checkSsl();
+ checkRealm();
+ authorizeClient();
+
+ RealmModel realm = session.getContext().getRealm();
+
+ AccessToken token = null;
+ try {
+ RSATokenVerifier verifier = RSATokenVerifier.create(reviewRequest.getSpec().getToken())
+ .realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
+
+ PublicKey publicKey = session.keys().getRsaPublicKey(realm, verifier.getHeader().getKeyId());
+ if (publicKey == null) {
+ error(401, Errors.INVALID_TOKEN, "Invalid public key");
+ } else {
+ verifier.publicKey(publicKey);
+ verifier.verify();
+ token = verifier.getToken();
+ }
+ } catch (VerificationException e) {
+ error(401, Errors.INVALID_TOKEN, "Token verification failure");
+ }
+
+ if (!tokenManager.isTokenValid(session, realm, token)) {
+ error(401, Errors.INVALID_TOKEN, "Token verification failure");
+ }
+
+ OpenShiftTokenReviewResponseRepresentation response = new OpenShiftTokenReviewResponseRepresentation();
+ response.getStatus().setAuthenticated(true);
+ response.getStatus().setUser(new OpenShiftTokenReviewResponseRepresentation.User());
+
+ OpenShiftTokenReviewResponseRepresentation.User userRep = response.getStatus().getUser();
+ userRep.setUid(token.getSubject());
+ userRep.setUsername(token.getPreferredUsername());
+
+ if (token.getScope() != null && !token.getScope().isEmpty()) {
+ OpenShiftTokenReviewResponseRepresentation.Extra extra = new OpenShiftTokenReviewResponseRepresentation.Extra();
+ extra.setScopes(token.getScope().split(" "));
+ userRep.setExtra(extra);
+ }
+
+ if (token.getOtherClaims() != null && token.getOtherClaims().get("groups") != null) {
+ List<String> groups = (List<String>) token.getOtherClaims().get("groups");
+ userRep.setGroups(groups);
+ }
+
+ event.success();
+ return Response.ok(response, MediaType.APPLICATION_JSON).build();
+ }
+
+ private void checkSsl() {
+ if (!session.getContext().getUri().getBaseUri().getScheme().equals("https") && session.getContext().getRealm().getSslRequired().isRequired(session.getContext().getConnection())) {
+ error(401, Errors.SSL_REQUIRED, null);
+ }
+ }
+
+ private void checkRealm() {
+ if (!session.getContext().getRealm().isEnabled()) {
+ error(401, Errors.REALM_DISABLED,null);
+ }
+ }
+
+ private void authorizeClient() {
+ try {
+ ClientModel client = AuthorizeClientUtil.authorizeClient(session, event).getClient();
+ event.client(client);
+
+ if (client == null || client.isPublicClient()) {
+ error(401, Errors.INVALID_CLIENT, "Public client is not permitted to invoke token review endpoint");
+ }
+
+ } catch (ErrorResponseException ere) {
+ error(401, Errors.INVALID_CLIENT_CREDENTIALS, ere.getErrorDescription());
+ } catch (Exception e) {
+ error(401, Errors.INVALID_CLIENT_CREDENTIALS, null);
+ }
+ }
+
+ private void error(int statusCode, String error, String description) {
+ OpenShiftTokenReviewResponseRepresentation rep = new OpenShiftTokenReviewResponseRepresentation();
+ rep.getStatus().setAuthenticated(false);
+
+ Response response = Response.status(statusCode).entity(rep).type(MediaType.APPLICATION_JSON_TYPE).build();
+
+ event.error(error);
+ event.detail(Details.REASON, description);
+
+ throw new ErrorResponseException(response);
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewEndpointFactory.java b/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewEndpointFactory.java
new file mode 100644
index 0000000..a58b8ce
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewEndpointFactory.java
@@ -0,0 +1,46 @@
+/*
+ * 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.protocol.openshift;
+
+import org.keycloak.common.Profile;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.protocol.oidc.ext.OIDCExtProvider;
+import org.keycloak.protocol.oidc.ext.OIDCExtProviderFactory;
+import org.keycloak.provider.EnvironmentDependentProviderFactory;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class OpenShiftTokenReviewEndpointFactory implements OIDCExtProviderFactory, EnvironmentDependentProviderFactory {
+
+ @Override
+ public OIDCExtProvider create(KeycloakSession session) {
+ return new OpenShiftTokenReviewEndpoint(session);
+ }
+
+ @Override
+ public String getId() {
+ return "openshift-token-review";
+ }
+
+ @Override
+ public boolean isSupported() {
+ return Profile.isFeatureEnabled(Profile.Feature.OPENSHIFT_INTEGRATION);
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewRequestRepresentation.java b/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewRequestRepresentation.java
new file mode 100755
index 0000000..cb62e07
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewRequestRepresentation.java
@@ -0,0 +1,80 @@
+/*
+ * 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.protocol.openshift;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.io.Serializable;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class OpenShiftTokenReviewRequestRepresentation implements Serializable {
+
+ @JsonProperty("apiVersion")
+ private String apiVersion = "authentication.k8s.io/v1beta1";
+
+ @JsonProperty("kind")
+ private String kind = "TokenReview";
+
+ @JsonProperty("spec")
+ private Spec spec;
+
+ public String getApiVersion() {
+ return apiVersion;
+ }
+
+ public void setApiVersion(String apiVersion) {
+ this.apiVersion = apiVersion;
+ }
+
+ public String getKind() {
+ return kind;
+ }
+
+ public void setKind(String kind) {
+ this.kind = kind;
+ }
+
+ public Spec getSpec() {
+ return spec;
+ }
+
+ public void setSpec(Spec spec) {
+ this.spec = spec;
+ }
+
+ public static class Spec implements Serializable {
+
+ @JsonProperty("token")
+ private String token;
+
+ public String getToken() {
+ return token;
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewResponseRepresentation.java b/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewResponseRepresentation.java
new file mode 100755
index 0000000..47918d2
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewResponseRepresentation.java
@@ -0,0 +1,154 @@
+/*
+ * 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.protocol.openshift;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class OpenShiftTokenReviewResponseRepresentation implements Serializable {
+
+ @JsonProperty("apiVersion")
+ private String apiVersion = "authentication.k8s.io/v1beta1";
+
+ @JsonProperty("kind")
+ private String kind = "TokenReview";
+
+ @JsonProperty("status")
+ private Status status = new Status();
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public void setStatus(Status status) {
+ this.status = status;
+ }
+
+ public String getApiVersion() {
+ return apiVersion;
+ }
+
+ public void setApiVersion(String apiVersion) {
+ this.apiVersion = apiVersion;
+ }
+
+ public String getKind() {
+ return kind;
+ }
+
+ public void setKind(String kind) {
+ this.kind = kind;
+ }
+
+ public static class Status implements Serializable {
+
+ @JsonProperty("authenticated")
+ private boolean authenticated;
+
+ @JsonProperty("user")
+ protected User user;
+
+ public boolean isAuthenticated() {
+ return authenticated;
+ }
+
+ public void setAuthenticated(boolean authenticated) {
+ this.authenticated = authenticated;
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+ public void setUser(User user) {
+ this.user = user;
+ }
+
+ }
+
+ public static class User implements Serializable {
+
+ @JsonProperty("username")
+ protected String username;
+
+ @JsonProperty("uid")
+ protected String uid;
+
+ @JsonProperty("groups")
+ protected List<String> groups = new LinkedList<>();
+
+ @JsonProperty("extra")
+ protected Extra extra;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getUid() {
+ return uid;
+ }
+
+ public void setUid(String uid) {
+ this.uid = uid;
+ }
+
+ public List<String> getGroups() {
+ return groups;
+ }
+
+ public void setGroups(List<String> groups) {
+ this.groups = groups;
+ }
+
+ public Extra getExtra() {
+ return extra;
+ }
+
+ public void setExtra(Extra extra) {
+ this.extra = extra;
+ }
+
+ }
+
+ public static class Extra implements Serializable {
+
+ @JsonProperty("scopes.authorization.openshift.io")
+ private String[] scopes;
+
+ public String[] getScopes() {
+ return scopes;
+ }
+
+ public void setScopes(String[] scopes) {
+ this.scopes = scopes;
+ }
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
index 6cabf76..09c2378 100644
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
@@ -106,6 +106,12 @@ public class DefaultKeycloakSession implements KeycloakSession {
}
@Override
+ public <T> T getAttribute(String attribute, Class<T> clazz) {
+ Object value = getAttribute(attribute);
+ return value != null && clazz.isInstance(value) ? (T) value : null;
+ }
+
+ @Override
public Object removeAttribute(String attribute) {
return attributes.remove(attribute);
}
diff --git a/services/src/main/java/org/keycloak/services/ErrorResponseException.java b/services/src/main/java/org/keycloak/services/ErrorResponseException.java
index 07c3bf5..e94bbaf 100644
--- a/services/src/main/java/org/keycloak/services/ErrorResponseException.java
+++ b/services/src/main/java/org/keycloak/services/ErrorResponseException.java
@@ -28,20 +28,37 @@ import javax.ws.rs.core.Response;
*/
public class ErrorResponseException extends WebApplicationException {
+ private final Response response;
private final String error;
private final String errorDescription;
private final Response.Status status;
public ErrorResponseException(String error, String errorDescription, Response.Status status) {
+ this.response = null;
this.error = error;
this.errorDescription = errorDescription;
this.status = status;
}
+ public ErrorResponseException(Response response) {
+ this.response = response;
+ this.error = null;
+ this.errorDescription = null;
+ this.status = null;
+ }
+
+ public String getErrorDescription() {
+ return errorDescription;
+ }
+
@Override
public Response getResponse() {
- OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription);
- return Response.status(status).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build();
+ if (response != null) {
+ return response;
+ } else {
+ OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription);
+ return Response.status(status).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build();
+ }
}
}
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.ext.OIDCExtProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.ext.OIDCExtProviderFactory
new file mode 100644
index 0000000..3c884ef
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.ext.OIDCExtProviderFactory
@@ -0,0 +1 @@
+org.keycloak.protocol.openshift.OpenShiftTokenReviewEndpointFactory
\ No newline at end of file
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 7027379..20e9747 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -21,3 +21,4 @@ org.keycloak.services.clientregistration.ClientRegistrationSpi
org.keycloak.services.clientregistration.policy.ClientRegistrationPolicySpi
org.keycloak.authentication.actiontoken.ActionTokenHandlerSpi
org.keycloak.services.x509.X509ClientCertificateLookupSpi
+org.keycloak.protocol.oidc.ext.OIDCExtSPI
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/DummyClientAuthenticator.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/DummyClientAuthenticator.java
new file mode 100755
index 0000000..d4a6c6b
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/DummyClientAuthenticator.java
@@ -0,0 +1,117 @@
+/*
+ * 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.testsuite.forms;
+
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.ClientAuthenticationFlowContext;
+import org.keycloak.authentication.FlowStatus;
+import org.keycloak.authentication.authenticators.client.AbstractClientAuthenticator;
+import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class DummyClientAuthenticator extends AbstractClientAuthenticator {
+
+ public static final String PROVIDER_ID = "testsuite-client-dummy";
+
+ public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+ AuthenticationExecutionModel.Requirement.ALTERNATIVE
+ };
+
+ @Override
+ public void authenticateClient(ClientAuthenticationFlowContext context) {
+ ClientIdAndSecretAuthenticator authenticator = new ClientIdAndSecretAuthenticator();
+ authenticator.authenticateClient(context);
+ if (context.getStatus().equals(FlowStatus.SUCCESS)) {
+ return;
+ }
+
+ String clientId = context.getUriInfo().getQueryParameters().getFirst("client_id");
+
+ if (clientId == null) {
+ clientId = context.getSession().getAttribute("client_id", String.class);
+ }
+
+ ClientModel client = context.getRealm().getClientByClientId(clientId);
+ if (client == null) {
+ context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND, null);
+ return;
+ }
+
+ context.getEvent().client(client);
+ context.setClient(client);
+ context.success();
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Testsuite ClientId Dummy";
+ }
+
+ @Override
+ public boolean isConfigurable() {
+ return false;
+ }
+
+ @Override
+ public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+ return REQUIREMENT_CHOICES;
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Dummy client authenticator, which authenticates the client with clientId only";
+ }
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return new LinkedList<>();
+ }
+
+ @Override
+ public List<ProviderConfigProperty> getConfigPropertiesPerClient() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Map<String, Object> getAdapterConfiguration(ClientModel client) {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public Set<String> getProtocolAuthenticatorMethods(String loginProtocol) {
+ return Collections.emptySet();
+ }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java
index b5efe23..5088e7a 100755
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java
@@ -41,7 +41,9 @@ public class PassThroughClientAuthenticator extends AbstractClientAuthenticator
public static String clientId = "test-app";
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
- AuthenticationExecutionModel.Requirement.REQUIRED
+ AuthenticationExecutionModel.Requirement.REQUIRED,
+ AuthenticationExecutionModel.Requirement.ALTERNATIVE,
+ AuthenticationExecutionModel.Requirement.DISABLED
};
private static final List<ProviderConfigProperty> clientConfigProperties = new ArrayList<ProviderConfigProperty>();
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory
index 05ea07c..67616c4 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory
@@ -15,4 +15,5 @@
# limitations under the License.
#
-org.keycloak.testsuite.forms.PassThroughClientAuthenticator
\ No newline at end of file
+org.keycloak.testsuite.forms.PassThroughClientAuthenticator
+org.keycloak.testsuite.forms.DummyClientAuthenticator
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index a257096..9b75790 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -148,6 +148,8 @@ public class OAuthClient {
private String codeChallengeMethod;
private String origin;
+ private boolean openid = true;
+
private Supplier<CloseableHttpClient> httpClient = OAuthClient::newCloseableHttpClient;
public class LogoutUrlBuilder {
@@ -212,6 +214,7 @@ public class OAuthClient {
codeChallenge = null;
codeChallengeMethod = null;
origin = null;
+ openid = true;
}
public void setDriver(WebDriver driver) {
@@ -773,8 +776,10 @@ public class OAuthClient {
b.queryParam(OIDCLoginProtocol.NONCE_PARAM, nonce);
}
- String scopeParam = TokenUtil.attachOIDCScope(scope);
- b.queryParam(OAuth2Constants.SCOPE, scopeParam);
+ String scopeParam = openid ? TokenUtil.attachOIDCScope(scope) : scope;
+ if (scopeParam != null && !scopeParam.isEmpty()) {
+ b.queryParam(OAuth2Constants.SCOPE, scopeParam);
+ }
if (maxAge != null) {
b.queryParam(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge);
@@ -883,6 +888,11 @@ public class OAuthClient {
return this;
}
+ public OAuthClient openid(boolean openid) {
+ this.openid = openid;
+ return this;
+ }
+
public OAuthClient uiLocales(String uiLocales){
this.uiLocales = uiLocales;
return this;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
index 0aa53e0..794199b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
@@ -81,12 +81,13 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"'client_secret' sent either in request parameters or in 'Authorization: Basic' header");
addProviderInfo(expected, "testsuite-client-passthrough", "Testsuite Dummy Client Validation", "Testsuite dummy authenticator, " +
"which automatically authenticates hardcoded client (like 'test-app' )");
+ addProviderInfo(expected, "testsuite-client-dummy", "Testsuite ClientId Dummy",
+ "Dummy client authenticator, which authenticates the client with clientId only");
addProviderInfo(expected, "client-x509", "X509 Certificate",
"Validates client based on a X509 Certificate");
addProviderInfo(expected, "client-secret-jwt", "Signed Jwt with Client Secret",
"Validates client based on signed JWT issued by client and signed with the Client Secret");
-
compareProviders(expected, result);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenShiftTokenReviewEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenShiftTokenReviewEndpointTest.java
new file mode 100644
index 0000000..48006ca
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenShiftTokenReviewEndpointTest.java
@@ -0,0 +1,354 @@
+package org.keycloak.testsuite.openshift;
+
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.broker.provider.util.SimpleHttp;
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.events.Details;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.protocol.openshift.OpenShiftTokenReviewRequestRepresentation;
+import org.keycloak.protocol.openshift.OpenShiftTokenReviewResponseRepresentation;
+import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientScopeRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.UserBuilder;
+
+import javax.ws.rs.core.Response;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;
+import static org.junit.Assert.*;
+
+public class OpenShiftTokenReviewEndpointTest extends AbstractTestRealmKeycloakTest {
+
+ private static boolean flowConfigured;
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+ @Override
+ public void configureTestRealm(RealmRepresentation testRealm) {
+ ClientRepresentation client = testRealm.getClients().stream().filter(r -> r.getClientId().equals("test-app")).findFirst().get();
+
+ List<ProtocolMapperRepresentation> mappers = new LinkedList<>();
+ ProtocolMapperRepresentation mapper = new ProtocolMapperRepresentation();
+ mapper.setName("groups");
+ mapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID);
+ mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ Map<String, String> config = new HashMap<>();
+ config.put("full.path", "false");
+ config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "groups");
+ config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
+ config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
+ mapper.setConfig(config);
+ mappers.add(mapper);
+
+ client.setProtocolMappers(mappers);
+ client.setPublicClient(false);
+ client.setClientAuthenticatorType("testsuite-client-dummy");
+
+ testRealm.getUsers().add(UserBuilder.create().username("groups-user").password("password").addGroups("/topGroup", "/topGroup/level2group").build());
+ }
+
+ @Before
+ public void enablePassthroughAuthenticator() {
+ if (!flowConfigured) {
+ HashMap<String, String> data = new HashMap<>();
+ data.put("newName", "testsuite-client-dummy");
+ Response response = testRealm().flows().copy("clients", data);
+ assertEquals(201, response.getStatus());
+ response.close();
+
+ data = new HashMap<>();
+ data.put("provider", "testsuite-client-dummy");
+ data.put("requirement", "ALTERNATIVE");
+
+ testRealm().flows().addExecution("testsuite-client-dummy", data);
+
+ RealmRepresentation realmRep = testRealm().toRepresentation();
+ realmRep.setClientAuthenticationFlow("testsuite-client-dummy");
+ testRealm().update(realmRep);
+
+ List<AuthenticationExecutionInfoRepresentation> executions = testRealm().flows().getExecutions("testsuite-client-dummy");
+ for (AuthenticationExecutionInfoRepresentation e : executions) {
+ if (e.getProviderId().equals("testsuite-client-dummy")) {
+ e.setRequirement("ALTERNATIVE");
+ testRealm().flows().updateExecutions("testsuite-client-dummy", e);
+ }
+ }
+ flowConfigured = true;
+ }
+ }
+
+ @Test
+ public void basicTest() {
+ Review r = new Review().invoke()
+ .assertSuccess();
+
+ String userId = testRealm().users().search(r.username).get(0).getId();
+
+ OpenShiftTokenReviewResponseRepresentation.User user = r.response.getStatus().getUser();
+
+ assertEquals(userId, user.getUid());
+ assertEquals("test-user@localhost", user.getUsername());
+ assertNotNull(user.getExtra());
+
+ r.assertScope("openid", "email", "profile");
+ }
+
+ @Test
+ public void groups() {
+ new Review().username("groups-user")
+ .invoke()
+ .assertSuccess().assertGroups("topGroup", "level2group");
+ }
+
+ @Test
+ public void customScopes() {
+ ClientScopeRepresentation clientScope = new ClientScopeRepresentation();
+ clientScope.setProtocol("openid-connect");
+ clientScope.setId("user:info");
+ clientScope.setName("user:info");
+
+ testRealm().clientScopes().create(clientScope);
+
+ ClientRepresentation clientRep = testRealm().clients().findByClientId("test-app").get(0);
+
+ testRealm().clients().get(clientRep.getId()).addOptionalClientScope("user:info");
+
+ try {
+ oauth.scope("user:info");
+ new Review()
+ .invoke()
+ .assertSuccess().assertScope("openid", "user:info", "profile", "email");
+ } finally {
+ testRealm().clients().get(clientRep.getId()).removeOptionalClientScope("user:info");
+ }
+ }
+
+ @Test
+ public void emptyScope() {
+ ClientRepresentation clientRep = testRealm().clients().findByClientId("test-app").get(0);
+
+ List<String> scopes = new LinkedList<>();
+ for (ClientScopeRepresentation s : testRealm().clients().get(clientRep.getId()).getDefaultClientScopes()) {
+ scopes.add(s.getId());
+ }
+
+ for (String s : scopes) {
+ testRealm().clients().get(clientRep.getId()).removeDefaultClientScope(s);
+ }
+
+ oauth.openid(false);
+ try {
+ new Review()
+ .invoke()
+ .assertSuccess().assertEmptyScope();
+ } finally {
+ oauth.openid(true);
+
+ for (String s : scopes) {
+ testRealm().clients().get(clientRep.getId()).addDefaultClientScope(s);
+ }
+ }
+ }
+
+ @Test
+ public void expiredToken() {
+ try {
+ new Review()
+ .runAfterTokenRequest(i -> setTimeOffset(testRealm().toRepresentation().getAccessTokenLifespan() + 10))
+ .invoke()
+ .assertError(401, "Token verification failure");
+ } finally {
+ resetTimeOffset();
+ }
+ }
+
+ @Test
+ public void invalidPublicKey() {
+ new Review()
+ .runAfterTokenRequest(i -> {
+ String header = i.token.split("\\.")[0];
+ String s = new String(Base64Url.decode(header));
+ s = s.replace(",\"kid\" : \"", ",\"kid\" : \"x");
+ String newHeader = Base64Url.encode(s.getBytes());
+ i.token = i.token.replaceFirst(header, newHeader);
+ })
+ .invoke()
+ .assertError(401, "Invalid public key");
+ }
+
+ @Test
+ public void noUserSession() {
+ new Review()
+ .runAfterTokenRequest(i -> {
+ String userId = testRealm().users().search(i.username).get(0).getId();
+ testRealm().users().get(userId).logout();
+ })
+ .invoke()
+ .assertError(401, "Token verification failure");
+ }
+
+ @Test
+ public void invalidTokenSignature() {
+ new Review()
+ .runAfterTokenRequest(i -> i.token += "x")
+ .invoke()
+ .assertError(401, "Token verification failure");
+ }
+
+ @Test
+ public void realmDisabled() {
+ RealmRepresentation r = testRealm().toRepresentation();
+ try {
+ new Review().runAfterTokenRequest(i -> {
+ r.setEnabled(false);
+ testRealm().update(r);
+ }).invoke().assertError(401, null);
+
+
+ } finally {
+ r.setEnabled(true);
+ testRealm().update(r);
+ }
+ }
+
+ @Test
+ public void publicClientNotPermitted() {
+ ClientRepresentation clientRep = testRealm().clients().findByClientId("test-app").get(0);
+ clientRep.setPublicClient(true);
+ testRealm().clients().get(clientRep.getId()).update(clientRep);
+ try {
+ new Review().invoke().assertError(401, "Public client is not permitted to invoke token review endpoint");
+ } finally {
+ clientRep.setPublicClient(false);
+ testRealm().clients().get(clientRep.getId()).update(clientRep);
+ }
+ }
+
+ private class Review {
+
+ private String realm = "test";
+ private String clientId = "test-app";
+ private String username = "test-user@localhost";
+ private String password = "password";
+ private InvokeRunnable runAfterTokenRequest;
+
+ private String token;
+ private int responseStatus;
+ private OpenShiftTokenReviewResponseRepresentation response;
+
+ public Review username(String username) {
+ this.username = username;
+ return this;
+ }
+
+ public Review runAfterTokenRequest(InvokeRunnable runnable) {
+ this.runAfterTokenRequest = runnable;
+ return this;
+ }
+
+ public Review invoke() {
+ try {
+ String userId = testRealm().users().search(username).get(0).getId();
+ oauth.doLogin(username, password);
+ EventRepresentation loginEvent = events.expectLogin().user(userId).assertEvent();
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password");
+
+ events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId()).detail("client_auth_method", "testsuite-client-dummy").user(userId).assertEvent();
+
+ token = accessTokenResponse.getAccessToken();
+
+ if (runAfterTokenRequest != null) {
+ runAfterTokenRequest.run(this);
+ }
+
+ CloseableHttpClient client = HttpClientBuilder.create().build();
+
+ String url = AuthServerTestEnricher.getAuthServerContextRoot() + "/auth/realms/" + realm +"/protocol/openid-connect/ext/openshift-token-review/" + clientId;
+
+ OpenShiftTokenReviewRequestRepresentation request = new OpenShiftTokenReviewRequestRepresentation();
+ OpenShiftTokenReviewRequestRepresentation.Spec spec = new OpenShiftTokenReviewRequestRepresentation.Spec();
+ spec.setToken(token);
+ request.setSpec(spec);
+
+ SimpleHttp.Response r = SimpleHttp.doPost(url, client).json(request).asResponse();
+
+ responseStatus = r.getStatus();
+ response = r.asJson(OpenShiftTokenReviewResponseRepresentation.class);
+
+ assertEquals("authentication.k8s.io/v1beta1", response.getApiVersion());
+ assertEquals("TokenReview", response.getKind());
+
+ client.close();
+
+ return this;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Review assertSuccess() {
+ assertEquals(200, responseStatus);
+ assertTrue(response.getStatus().isAuthenticated());
+ assertNotNull(response.getStatus().getUser());
+ return this;
+ }
+
+ private Review assertError(int expectedStatus, String expectedReason) {
+ assertEquals(expectedStatus, responseStatus);
+ assertFalse(response.getStatus().isAuthenticated());
+ assertNull(response.getStatus().getUser());
+
+ if (expectedReason != null) {
+ EventRepresentation poll = events.poll();
+ assertEquals(expectedReason, poll.getDetails().get(Details.REASON));
+ }
+
+ return this;
+ }
+
+ private void assertScope(String... expectedScope) {
+ List<String> actualScopes = Arrays.asList(response.getStatus().getUser().getExtra().getScopes());
+ assertEquals(expectedScope.length, actualScopes.size());
+ assertThat(actualScopes, containsInAnyOrder(expectedScope));
+ }
+
+ private void assertEmptyScope() {
+ assertNull(response.getStatus().getUser().getExtra());
+ }
+
+ private void assertGroups(String... expectedGroups) {
+ List<String> actualGroups = new LinkedList<>(response.getStatus().getUser().getGroups());
+ assertEquals(expectedGroups.length, actualGroups.size());
+ assertThat(actualGroups, containsInAnyOrder(expectedGroups));
+ }
+
+ }
+
+ private interface InvokeRunnable {
+ void run(Review i);
+ }
+
+}