KEYCLOAK-6229 OpenShift Token Review interface
This commit is contained in:
parent
1fb4ca4525
commit
bf758809ba
22 changed files with 1073 additions and 8 deletions
|
@ -33,7 +33,13 @@ import java.util.Set;
|
||||||
public class Profile {
|
public class Profile {
|
||||||
|
|
||||||
public enum Feature {
|
public enum Feature {
|
||||||
AUTHORIZATION, IMPERSONATION, SCRIPTS, DOCKER, ACCOUNT2, TOKEN_EXCHANGE
|
ACCOUNT2,
|
||||||
|
AUTHORIZATION,
|
||||||
|
DOCKER,
|
||||||
|
IMPERSONATION,
|
||||||
|
OPENSHIFT_INTEGRATION,
|
||||||
|
SCRIPTS,
|
||||||
|
TOKEN_EXCHANGE
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum ProductValue {
|
private enum ProductValue {
|
||||||
|
|
|
@ -76,6 +76,8 @@ public interface KeycloakSession {
|
||||||
Class<? extends Provider> getProviderClass(String providerClassName);
|
Class<? extends Provider> getProviderClass(String providerClassName);
|
||||||
|
|
||||||
Object getAttribute(String attribute);
|
Object getAttribute(String attribute);
|
||||||
|
<T> T getAttribute(String attribute, Class<T> clazz);
|
||||||
|
|
||||||
Object removeAttribute(String attribute);
|
Object removeAttribute(String attribute);
|
||||||
void setAttribute(String name, Object value);
|
void setAttribute(String name, Object value);
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,10 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
|
||||||
clientSecret = formData.getFirst(OAuth2Constants.CLIENT_SECRET);
|
clientSecret = formData.getFirst(OAuth2Constants.CLIENT_SECRET);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client_id == null) {
|
||||||
|
client_id = context.getSession().getAttribute("client_id", String.class);
|
||||||
|
}
|
||||||
|
|
||||||
if (client_id == null) {
|
if (client_id == null) {
|
||||||
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Missing client_id parameter");
|
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Missing client_id parameter");
|
||||||
context.challenge(challengeResponse);
|
context.challenge(challengeResponse);
|
||||||
|
|
|
@ -66,6 +66,10 @@ public class X509ClientAuthenticator extends AbstractClientAuthenticator {
|
||||||
client_id = queryParams.getFirst(OAuth2Constants.CLIENT_ID);
|
client_id = queryParams.getFirst(OAuth2Constants.CLIENT_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client_id == null) {
|
||||||
|
client_id = context.getSession().getAttribute("client_id", String.class);
|
||||||
|
}
|
||||||
|
|
||||||
if (client_id == null) {
|
if (client_id == null) {
|
||||||
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Missing client_id parameter");
|
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Missing client_id parameter");
|
||||||
context.challenge(challengeResponse);
|
context.challenge(challengeResponse);
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.keycloak.protocol.oidc.endpoints.LoginStatusIframeEndpoint;
|
||||||
import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint;
|
import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint;
|
||||||
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
|
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
|
||||||
import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint;
|
import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint;
|
||||||
|
import org.keycloak.protocol.oidc.ext.OIDCExtProvider;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.Cors;
|
import org.keycloak.services.resources.Cors;
|
||||||
|
@ -42,8 +43,10 @@ import org.keycloak.services.resources.RealmsResource;
|
||||||
import org.keycloak.services.util.CacheControlUtil;
|
import org.keycloak.services.util.CacheControlUtil;
|
||||||
|
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.NotFoundException;
|
||||||
import javax.ws.rs.OPTIONS;
|
import javax.ws.rs.OPTIONS;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.Context;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -105,6 +105,12 @@ public class DefaultKeycloakSession implements KeycloakSession {
|
||||||
return attributes.get(attribute);
|
return attributes.get(attribute);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getAttribute(String attribute, Class<T> clazz) {
|
||||||
|
Object value = getAttribute(attribute);
|
||||||
|
return value != null && clazz.isInstance(value) ? (T) value : null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object removeAttribute(String attribute) {
|
public Object removeAttribute(String attribute) {
|
||||||
return attributes.remove(attribute);
|
return attributes.remove(attribute);
|
||||||
|
|
|
@ -28,20 +28,37 @@ import javax.ws.rs.core.Response;
|
||||||
*/
|
*/
|
||||||
public class ErrorResponseException extends WebApplicationException {
|
public class ErrorResponseException extends WebApplicationException {
|
||||||
|
|
||||||
|
private final Response response;
|
||||||
private final String error;
|
private final String error;
|
||||||
private final String errorDescription;
|
private final String errorDescription;
|
||||||
private final Response.Status status;
|
private final Response.Status status;
|
||||||
|
|
||||||
public ErrorResponseException(String error, String errorDescription, Response.Status status) {
|
public ErrorResponseException(String error, String errorDescription, Response.Status status) {
|
||||||
|
this.response = null;
|
||||||
this.error = error;
|
this.error = error;
|
||||||
this.errorDescription = errorDescription;
|
this.errorDescription = errorDescription;
|
||||||
this.status = status;
|
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
|
@Override
|
||||||
public Response getResponse() {
|
public Response getResponse() {
|
||||||
|
if (response != null) {
|
||||||
|
return response;
|
||||||
|
} else {
|
||||||
OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription);
|
OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription);
|
||||||
return Response.status(status).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build();
|
return Response.status(status).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
org.keycloak.protocol.openshift.OpenShiftTokenReviewEndpointFactory
|
|
@ -21,3 +21,4 @@ org.keycloak.services.clientregistration.ClientRegistrationSpi
|
||||||
org.keycloak.services.clientregistration.policy.ClientRegistrationPolicySpi
|
org.keycloak.services.clientregistration.policy.ClientRegistrationPolicySpi
|
||||||
org.keycloak.authentication.actiontoken.ActionTokenHandlerSpi
|
org.keycloak.authentication.actiontoken.ActionTokenHandlerSpi
|
||||||
org.keycloak.services.x509.X509ClientCertificateLookupSpi
|
org.keycloak.services.x509.X509ClientCertificateLookupSpi
|
||||||
|
org.keycloak.protocol.oidc.ext.OIDCExtSPI
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,7 +41,9 @@ public class PassThroughClientAuthenticator extends AbstractClientAuthenticator
|
||||||
public static String clientId = "test-app";
|
public static String clientId = "test-app";
|
||||||
|
|
||||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
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>();
|
private static final List<ProviderConfigProperty> clientConfigProperties = new ArrayList<ProviderConfigProperty>();
|
||||||
|
|
|
@ -16,3 +16,4 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
org.keycloak.testsuite.forms.PassThroughClientAuthenticator
|
org.keycloak.testsuite.forms.PassThroughClientAuthenticator
|
||||||
|
org.keycloak.testsuite.forms.DummyClientAuthenticator
|
|
@ -148,6 +148,8 @@ public class OAuthClient {
|
||||||
private String codeChallengeMethod;
|
private String codeChallengeMethod;
|
||||||
private String origin;
|
private String origin;
|
||||||
|
|
||||||
|
private boolean openid = true;
|
||||||
|
|
||||||
private Supplier<CloseableHttpClient> httpClient = OAuthClient::newCloseableHttpClient;
|
private Supplier<CloseableHttpClient> httpClient = OAuthClient::newCloseableHttpClient;
|
||||||
|
|
||||||
public class LogoutUrlBuilder {
|
public class LogoutUrlBuilder {
|
||||||
|
@ -212,6 +214,7 @@ public class OAuthClient {
|
||||||
codeChallenge = null;
|
codeChallenge = null;
|
||||||
codeChallengeMethod = null;
|
codeChallengeMethod = null;
|
||||||
origin = null;
|
origin = null;
|
||||||
|
openid = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDriver(WebDriver driver) {
|
public void setDriver(WebDriver driver) {
|
||||||
|
@ -773,8 +776,10 @@ public class OAuthClient {
|
||||||
b.queryParam(OIDCLoginProtocol.NONCE_PARAM, nonce);
|
b.queryParam(OIDCLoginProtocol.NONCE_PARAM, nonce);
|
||||||
}
|
}
|
||||||
|
|
||||||
String scopeParam = TokenUtil.attachOIDCScope(scope);
|
String scopeParam = openid ? TokenUtil.attachOIDCScope(scope) : scope;
|
||||||
|
if (scopeParam != null && !scopeParam.isEmpty()) {
|
||||||
b.queryParam(OAuth2Constants.SCOPE, scopeParam);
|
b.queryParam(OAuth2Constants.SCOPE, scopeParam);
|
||||||
|
}
|
||||||
|
|
||||||
if (maxAge != null) {
|
if (maxAge != null) {
|
||||||
b.queryParam(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge);
|
b.queryParam(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge);
|
||||||
|
@ -883,6 +888,11 @@ public class OAuthClient {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OAuthClient openid(boolean openid) {
|
||||||
|
this.openid = openid;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public OAuthClient uiLocales(String uiLocales){
|
public OAuthClient uiLocales(String uiLocales){
|
||||||
this.uiLocales = uiLocales;
|
this.uiLocales = uiLocales;
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -81,12 +81,13 @@ public class ProvidersTest extends AbstractAuthenticationTest {
|
||||||
"'client_secret' sent either in request parameters or in 'Authorization: Basic' header");
|
"'client_secret' sent either in request parameters or in 'Authorization: Basic' header");
|
||||||
addProviderInfo(expected, "testsuite-client-passthrough", "Testsuite Dummy Client Validation", "Testsuite dummy authenticator, " +
|
addProviderInfo(expected, "testsuite-client-passthrough", "Testsuite Dummy Client Validation", "Testsuite dummy authenticator, " +
|
||||||
"which automatically authenticates hardcoded client (like 'test-app' )");
|
"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",
|
addProviderInfo(expected, "client-x509", "X509 Certificate",
|
||||||
"Validates client based on a X509 Certificate");
|
"Validates client based on a X509 Certificate");
|
||||||
addProviderInfo(expected, "client-secret-jwt", "Signed Jwt with Client Secret",
|
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");
|
"Validates client based on signed JWT issued by client and signed with the Client Secret");
|
||||||
|
|
||||||
|
|
||||||
compareProviders(expected, result);
|
compareProviders(expected, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue