KEYCLOAK-3058 Support for validation of "aud" in adapters through verify-token-audience configuration switch

This commit is contained in:
mposolda 2018-09-14 09:45:00 +02:00 committed by Marek Posolda
parent adf0a19f9d
commit 3777dc45d0
51 changed files with 1004 additions and 128 deletions

View file

@ -482,6 +482,16 @@ public class AdapterDeploymentContext {
public void setPublicKeyCacheTtl(int publicKeyCacheTtl) { public void setPublicKeyCacheTtl(int publicKeyCacheTtl) {
delegate.setPublicKeyCacheTtl(publicKeyCacheTtl); delegate.setPublicKeyCacheTtl(publicKeyCacheTtl);
} }
@Override
public boolean isVerifyTokenAudience() {
return delegate.isVerifyTokenAudience();
}
@Override
public void setVerifyTokenAudience(boolean verifyTokenAudience) {
delegate.setVerifyTokenAudience(verifyTokenAudience);
}
} }
protected KeycloakUriBuilder getBaseBuilder(HttpFacade facade, String base) { protected KeycloakUriBuilder getBaseBuilder(HttpFacade facade, String base) {

View file

@ -18,7 +18,7 @@
package org.keycloak.adapters; package org.keycloak.adapters;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.adapters.spi.AuthChallenge; import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome; import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.spi.HttpFacade;
@ -96,7 +96,7 @@ public class BearerTokenRequestAuthenticator {
} }
} }
try { try {
token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); token = AdapterTokenVerifier.verifyToken(tokenString, deployment);
} catch (VerificationException e) { } catch (VerificationException e) {
log.error("Failed to verify token", e); log.error("Failed to verify token", e);
challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, "invalid_token", e.getMessage()); challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, "invalid_token", e.getMessage());

View file

@ -19,7 +19,8 @@ package org.keycloak.adapters;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.KeycloakPrincipal; import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.TokenVerifier;
import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.common.VerificationException; import org.keycloak.common.VerificationException;
import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.KeycloakUriBuilder;
@ -71,7 +72,11 @@ public class CookieTokenStore {
try { try {
// Skip check if token is active now. It's supposed to be done later by the caller // Skip check if token is active now. It's supposed to be done later by the caller
AccessToken accessToken = AdapterRSATokenVerifier.verifyToken(accessTokenString, deployment, false, true); TokenVerifier<AccessToken> tokenVerifier = AdapterTokenVerifier.createVerifier(accessTokenString, deployment, true, AccessToken.class)
.checkActive(false)
.verify();
AccessToken accessToken = tokenVerifier.getToken();
IDToken idToken; IDToken idToken;
if (idTokenString != null && idTokenString.length() > 0) { if (idTokenString != null && idTokenString.length() > 0) {
try { try {

View file

@ -92,10 +92,11 @@ public class KeycloakDeployment {
// https://tools.ietf.org/html/rfc7636 // https://tools.ietf.org/html/rfc7636
protected boolean pkce = false; protected boolean pkce = false;
protected boolean ignoreOAuthQueryParameter; protected boolean ignoreOAuthQueryParameter;
protected Map<String, String> redirectRewriteRules; protected Map<String, String> redirectRewriteRules;
protected boolean delegateBearerErrorResponseSending = false; protected boolean delegateBearerErrorResponseSending = false;
protected boolean verifyTokenAudience = false;
public KeycloakDeployment() { public KeycloakDeployment() {
} }
@ -477,4 +478,12 @@ public class KeycloakDeployment {
public void setDelegateBearerErrorResponseSending(boolean delegateBearerErrorResponseSending) { public void setDelegateBearerErrorResponseSending(boolean delegateBearerErrorResponseSending) {
this.delegateBearerErrorResponseSending = delegateBearerErrorResponseSending; this.delegateBearerErrorResponseSending = delegateBearerErrorResponseSending;
} }
public boolean isVerifyTokenAudience() {
return verifyTokenAudience;
}
public void setVerifyTokenAudience(boolean verifyTokenAudience) {
this.verifyTokenAudience = verifyTokenAudience;
}
} }

View file

@ -122,6 +122,7 @@ public class KeycloakDeploymentBuilder {
deployment.setPublicKeyCacheTtl(adapterConfig.getPublicKeyCacheTtl()); deployment.setPublicKeyCacheTtl(adapterConfig.getPublicKeyCacheTtl());
deployment.setIgnoreOAuthQueryParameter(adapterConfig.isIgnoreOAuthQueryParameter()); deployment.setIgnoreOAuthQueryParameter(adapterConfig.isIgnoreOAuthQueryParameter());
deployment.setRewriteRedirectRules(adapterConfig.getRedirectRewriteRules()); deployment.setRewriteRedirectRules(adapterConfig.getRedirectRewriteRules());
deployment.setVerifyTokenAudience(adapterConfig.isVerifyTokenAudience());
if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) { if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) {
throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url"); throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url");

View file

@ -19,7 +19,7 @@ package org.keycloak.adapters;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.adapters.spi.AdapterSessionStore; import org.keycloak.adapters.spi.AdapterSessionStore;
import org.keycloak.adapters.spi.AuthChallenge; import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome; import org.keycloak.adapters.spi.AuthOutcome;
@ -40,7 +40,6 @@ import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.Map; import java.util.Map;
import java.util.logging.Level;
/** /**
@ -359,15 +358,9 @@ public class OAuthRequestAuthenticator {
} }
try { try {
token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenString, idTokenString, deployment);
if (idTokenString != null) { token = tokens.getAccessToken();
try { idToken = tokens.getIdToken();
JWSInput input = new JWSInput(idTokenString);
idToken = input.readJsonContent(IDToken.class);
} catch (JWSInputException e) {
throw new VerificationException();
}
}
log.debug("Token Verification succeeded!"); log.debug("Token Verification succeeded!");
} catch (VerificationException e) { } catch (VerificationException e) {
log.error("failed verification of token: " + e.getMessage()); log.error("failed verification of token: " + e.getMessage());

View file

@ -20,30 +20,27 @@ package org.keycloak.adapters;
import java.security.PublicKey; import java.security.PublicKey;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.TokenVerifier;
import org.keycloak.adapters.authentication.ClientCredentialsProvider; import org.keycloak.adapters.authentication.ClientCredentialsProvider;
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider; import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.spi.UserSessionManagement; import org.keycloak.adapters.spi.UserSessionManagement;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.StreamUtil; import org.keycloak.common.util.StreamUtil;
import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder; import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.jose.jws.JWSInputException; import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.VersionRepresentation; import org.keycloak.representations.VersionRepresentation;
import org.keycloak.constants.AdapterConstants; import org.keycloak.constants.AdapterConstants;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.VersionRepresentation;
import org.keycloak.representations.adapters.action.AdminAction; import org.keycloak.representations.adapters.action.AdminAction;
import org.keycloak.representations.adapters.action.LogoutAction; import org.keycloak.representations.adapters.action.LogoutAction;
import org.keycloak.representations.adapters.action.PushNotBeforeAction; import org.keycloak.representations.adapters.action.PushNotBeforeAction;
import org.keycloak.representations.adapters.action.TestAvailabilityAction; import org.keycloak.representations.adapters.action.TestAvailabilityAction;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import java.security.PublicKey;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
@ -213,17 +210,19 @@ public class PreAuthActionsHandler {
} }
try { try {
JWSInput input = new JWSInput(token); // Check just signature. Other things checked in validateAction
PublicKey publicKey = AdapterRSATokenVerifier.getPublicKey(input.getHeader().getKeyId(), deployment); TokenVerifier tokenVerifier = AdapterTokenVerifier.createVerifier(token, deployment, false, JsonWebToken.class);
if (RSAProvider.verify(input, publicKey)) { tokenVerifier.verify();
return input; return new JWSInput(token);
} catch (VerificationException ignore) {
log.warn("admin request failed, unable to verify token: " + ignore.getMessage());
if (log.isDebugEnabled()) {
log.debug(ignore.getMessage(), ignore);
} }
} catch (JWSInputException ignore) {
}
log.warn("admin request failed, unable to verify token"); facade.getResponse().sendError(403, "token failed verification");
facade.getResponse().sendError(403, "no token"); return null;
return null; }
} }

View file

@ -20,7 +20,7 @@ package org.keycloak.adapters;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.AuthorizationContext; import org.keycloak.AuthorizationContext;
import org.keycloak.KeycloakSecurityContext; import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.common.VerificationException; import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
@ -130,7 +130,8 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
String tokenString = response.getToken(); String tokenString = response.getToken();
AccessToken token = null; AccessToken token = null;
try { try {
token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenString, response.getIdToken(), deployment);
token = tokens.getAccessToken();
log.debug("Token Verification succeeded!"); log.debug("Token Verification succeeded!");
} catch (VerificationException e) { } catch (VerificationException e) {
log.error("failed verification of token"); log.error("failed verification of token");

View file

@ -27,7 +27,7 @@ import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext; import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OIDCHttpFacade; import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.authorization.client.AuthorizationDeniedException; import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient; import org.keycloak.authorization.client.AuthzClient;
@ -171,7 +171,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
} }
if (authzResponse != null) { if (authzResponse != null) {
return AdapterRSATokenVerifier.verifyToken(authzResponse.getToken(), deployment); return AdapterTokenVerifier.verifyToken(authzResponse.getToken(), deployment);
} }
} catch (AuthorizationDeniedException ignore) { } catch (AuthorizationDeniedException ignore) {
LOGGER.debug("Authorization denied", ignore); LOGGER.debug("Authorization denied", ignore);

View file

@ -23,7 +23,7 @@ import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.common.VerificationException; import org.keycloak.common.VerificationException;
import org.keycloak.common.util.FindFile; import org.keycloak.common.util.FindFile;
import org.keycloak.common.util.reflections.Reflections; import org.keycloak.common.util.reflections.Reflections;
@ -202,8 +202,16 @@ public abstract class AbstractKeycloakLoginModule implements LoginModule {
protected Auth bearerAuth(String tokenString) throws VerificationException { protected Auth bearerAuth(String tokenString) throws VerificationException {
AccessToken token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); AccessToken token = AdapterTokenVerifier.verifyToken(tokenString, deployment);
return postTokenVerification(tokenString, token);
}
/**
* Called after accessToken was verified (including signature, expiration etc)
*
*/
protected Auth postTokenVerification(String tokenString, AccessToken token) {
boolean verifyCaller; boolean verifyCaller;
if (deployment.isUseResourceRoleMappings()) { if (deployment.isUseResourceRoleMappings()) {
verifyCaller = token.isVerifyCaller(deployment.getResourceName()); verifyCaller = token.isVerifyCaller(deployment.getResourceName());

View file

@ -27,6 +27,7 @@ import org.apache.http.message.BasicNameValuePair;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils; import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.common.VerificationException; import org.keycloak.common.VerificationException;
import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.constants.ServiceUrlConstants;
@ -56,11 +57,15 @@ public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule {
private static final Logger log = Logger.getLogger(DirectAccessGrantsLoginModule.class); private static final Logger log = Logger.getLogger(DirectAccessGrantsLoginModule.class);
public static final String SCOPE_OPTION = "scope";
private String refreshToken; private String refreshToken;
private String scope;
@Override @Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) { public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
super.initialize(subject, callbackHandler, sharedState, options); super.initialize(subject, callbackHandler, sharedState, options);
this.scope = (String)options.get(SCOPE_OPTION);
// This is used just for logout // This is used just for logout
Iterator<RefreshTokenHolder> iterator = subject.getPrivateCredentials(RefreshTokenHolder.class).iterator(); Iterator<RefreshTokenHolder> iterator = subject.getPrivateCredentials(RefreshTokenHolder.class).iterator();
@ -89,6 +94,10 @@ public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule {
formparams.add(new BasicNameValuePair("username", username)); formparams.add(new BasicNameValuePair("username", username));
formparams.add(new BasicNameValuePair("password", password)); formparams.add(new BasicNameValuePair("password", password));
if (scope != null) {
formparams.add(new BasicNameValuePair(OAuth2Constants.SCOPE, scope));
}
ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams); ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
@ -121,7 +130,8 @@ public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule {
// refreshToken will be saved to privateCreds of Subject for now // refreshToken will be saved to privateCreds of Subject for now
refreshToken = tokenResponse.getRefreshToken(); refreshToken = tokenResponse.getRefreshToken();
return bearerAuth(tokenResponse.getToken()); AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenResponse.getToken(), tokenResponse.getIdToken(), deployment);
return postTokenVerification(tokenResponse.getToken(), tokens.getAccessToken());
} }
@Override @Override

View file

@ -1,58 +0,0 @@
/*
* 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.adapters.rotation;
import org.jboss.logging.Logger;
import org.keycloak.RSATokenVerifier;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.common.VerificationException;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.representations.AccessToken;
import java.security.PublicKey;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class AdapterRSATokenVerifier {
private static final Logger log = Logger.getLogger(AdapterRSATokenVerifier.class);
public static AccessToken verifyToken(String tokenString, KeycloakDeployment deployment) throws VerificationException {
return verifyToken(tokenString, deployment, true, true);
}
public static PublicKey getPublicKey(String kid, KeycloakDeployment deployment) throws VerificationException {
PublicKeyLocator pkLocator = deployment.getPublicKeyLocator();
PublicKey publicKey = pkLocator.getPublicKey(kid, deployment);
if (publicKey == null) {
log.errorf("Didn't find publicKey for kid: %s", kid);
throw new VerificationException("Didn't find publicKey for specified kid");
}
return publicKey;
}
public static AccessToken verifyToken(String tokenString, KeycloakDeployment deployment, boolean checkActive, boolean checkTokenType) throws VerificationException {
RSATokenVerifier verifier = RSATokenVerifier.create(tokenString).realmUrl(deployment.getRealmInfoUrl()).checkActive(checkActive).checkTokenType(checkTokenType);
PublicKey publicKey = getPublicKey(verifier.getHeader().getKeyId(), deployment);
return verifier.publicKey(publicKey).verify().getToken();
}
}

View file

@ -0,0 +1,149 @@
/*
* 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.adapters.rotation;
import org.jboss.logging.Logger;
import org.keycloak.TokenVerifier;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.common.VerificationException;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
import java.security.PublicKey;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class AdapterTokenVerifier {
private static final Logger log = Logger.getLogger(AdapterTokenVerifier.class);
/**
* Verifies bearer token. Typically called when bearer token (access token) is sent to the service, which wants to verify it. Hence it also checks the audience in the token.
*
* @param tokenString
* @param deployment
* @return
* @throws VerificationException
*/
public static AccessToken verifyToken(String tokenString, KeycloakDeployment deployment) throws VerificationException {
TokenVerifier<AccessToken> tokenVerifier = createVerifier(tokenString, deployment, true, AccessToken.class);
// Verify audience of bearer-token
if (deployment.isVerifyTokenAudience()) {
tokenVerifier.audience(deployment.getResourceName());
}
return tokenVerifier.verify().getToken();
}
/**
* Verify access token and ID token. Typically called after successful tokenResponse is received from Keycloak
*
* @param accessTokenString
* @param idTokenString
* @param deployment
* @return verified and parsed accessToken and idToken
* @throws VerificationException
*/
public static VerifiedTokens verifyTokens(String accessTokenString, String idTokenString, KeycloakDeployment deployment) throws VerificationException {
// Adapters currently do most of the checks including signature etc on the access token
TokenVerifier<AccessToken> tokenVerifier = createVerifier(accessTokenString, deployment, true, AccessToken.class);
AccessToken accessToken = tokenVerifier.verify().getToken();
if (idTokenString != null) {
// Don't verify signature again on IDToken
IDToken idToken = TokenVerifier.create(idTokenString, IDToken.class).getToken();
TokenVerifier<IDToken> idTokenVerifier = TokenVerifier.createWithoutSignature(idToken);
// Always verify audience on IDToken
idTokenVerifier.audience(deployment.getResourceName());
idTokenVerifier.verify();
return new VerifiedTokens(accessToken, idToken);
} else {
return new VerifiedTokens(accessToken, null);
}
}
/**
* Creates verifier, initializes it from the KeycloakDeployment and adds the publicKey and some default basic checks (activeness and tokenType). Useful if caller wants to add/remove/update
* some checks
*
* @param tokenString
* @param deployment
* @param withDefaultChecks
* @param tokenClass
* @param <T>
* @return tokenVerifier
* @throws VerificationException
*/
public static <T extends JsonWebToken> TokenVerifier<T> createVerifier(String tokenString, KeycloakDeployment deployment, boolean withDefaultChecks, Class<T> tokenClass) throws VerificationException {
TokenVerifier<T> tokenVerifier = TokenVerifier.create(tokenString, tokenClass);
if (withDefaultChecks) {
tokenVerifier
.withDefaultChecks()
.realmUrl(deployment.getRealmInfoUrl());
}
String kid = tokenVerifier.getHeader().getKeyId();
PublicKey publicKey = getPublicKey(kid, deployment);
tokenVerifier.publicKey(publicKey);
return tokenVerifier;
}
private static PublicKey getPublicKey(String kid, KeycloakDeployment deployment) throws VerificationException {
PublicKeyLocator pkLocator = deployment.getPublicKeyLocator();
PublicKey publicKey = pkLocator.getPublicKey(kid, deployment);
if (publicKey == null) {
log.errorf("Didn't find publicKey for kid: %s", kid);
throw new VerificationException("Didn't find publicKey for specified kid");
}
return publicKey;
}
public static class VerifiedTokens {
private final AccessToken accessToken;
private final IDToken idToken;
public VerifiedTokens(AccessToken accessToken, IDToken idToken) {
this.accessToken = accessToken;
this.idToken = idToken;
}
public AccessToken getAccessToken() {
return accessToken;
}
public IDToken getIdToken() {
return idToken;
}
}
}

View file

@ -77,6 +77,7 @@ public class KeycloakDeploymentBuilderTest {
assertEquals(20, deployment.getMinTimeBetweenJwksRequests()); assertEquals(20, deployment.getMinTimeBetweenJwksRequests());
assertEquals(120, deployment.getPublicKeyCacheTtl()); assertEquals(120, deployment.getPublicKeyCacheTtl());
assertEquals("/api/$1", deployment.getRedirectRewriteRules().get("^/wsmaster/api/(.*)$")); assertEquals("/api/$1", deployment.getRedirectRewriteRules().get("^/wsmaster/api/(.*)$"));
assertTrue(deployment.isVerifyTokenAudience());
} }
@Test @Test

View file

@ -34,6 +34,7 @@
"min-time-between-jwks-requests": 20, "min-time-between-jwks-requests": 20,
"public-key-cache-ttl": 120, "public-key-cache-ttl": 120,
"ignore-oauth-query-parameter": true, "ignore-oauth-query-parameter": true,
"verify-token-audience": true,
"redirect-rewrite-rules" : { "redirect-rewrite-rules" : {
"^/wsmaster/api/(.*)$" : "/api/$1" "^/wsmaster/api/(.*)$" : "/api/$1"
} }

View file

@ -98,6 +98,12 @@ class SecureDeploymentDefinition extends SimpleResourceDefinition {
.setValidator(new IntRangeValidator(-1, true)) .setValidator(new IntRangeValidator(-1, true))
.setAllowExpression(true) .setAllowExpression(true)
.build(); .build();
protected static final SimpleAttributeDefinition PUBLIC_KEY_CACHE_TTL =
new SimpleAttributeDefinitionBuilder("public-key-cache-ttl", ModelType.INT, true)
.setXmlName("public-key-cache-ttl")
.setAllowExpression(true)
.setValidator(new IntRangeValidator(-1, true))
.build();
protected static final List<SimpleAttributeDefinition> DEPLOYMENT_ONLY_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>(); protected static final List<SimpleAttributeDefinition> DEPLOYMENT_ONLY_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();
static { static {
@ -110,6 +116,7 @@ class SecureDeploymentDefinition extends SimpleResourceDefinition {
DEPLOYMENT_ONLY_ATTRIBUTES.add(TURN_OFF_CHANGE_SESSION); DEPLOYMENT_ONLY_ATTRIBUTES.add(TURN_OFF_CHANGE_SESSION);
DEPLOYMENT_ONLY_ATTRIBUTES.add(TOKEN_MINIMUM_TIME_TO_LIVE); DEPLOYMENT_ONLY_ATTRIBUTES.add(TOKEN_MINIMUM_TIME_TO_LIVE);
DEPLOYMENT_ONLY_ATTRIBUTES.add(MIN_TIME_BETWEEN_JWKS_REQUESTS); DEPLOYMENT_ONLY_ATTRIBUTES.add(MIN_TIME_BETWEEN_JWKS_REQUESTS);
DEPLOYMENT_ONLY_ATTRIBUTES.add(PUBLIC_KEY_CACHE_TTL);
} }
protected static final List<SimpleAttributeDefinition> ALL_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>(); protected static final List<SimpleAttributeDefinition> ALL_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();

View file

@ -173,6 +173,13 @@ class SharedAttributeDefinitons {
.setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true))
.build(); .build();
protected static final SimpleAttributeDefinition VERIFY_TOKEN_AUDIENCE =
new SimpleAttributeDefinitionBuilder("verify-token-audience", ModelType.BOOLEAN, true)
.setXmlName("verify-token-audience")
.setAllowExpression(true)
.setDefaultValue(new ModelNode(false))
.build();
protected static final List<SimpleAttributeDefinition> ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>(); protected static final List<SimpleAttributeDefinition> ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();
@ -200,6 +207,7 @@ class SharedAttributeDefinitons {
ATTRIBUTES.add(TOKEN_STORE); ATTRIBUTES.add(TOKEN_STORE);
ATTRIBUTES.add(PRINCIPAL_ATTRIBUTE); ATTRIBUTES.add(PRINCIPAL_ATTRIBUTE);
ATTRIBUTES.add(PROXY_URL); ATTRIBUTES.add(PROXY_URL);
ATTRIBUTES.add(VERIFY_TOKEN_AUDIENCE);
} }
/** /**

View file

@ -47,6 +47,7 @@ keycloak.realm.register-node-period=how often to re-register node
keycloak.realm.token-store=cookie or session storage for auth session data keycloak.realm.token-store=cookie or session storage for auth session data
keycloak.realm.principal-attribute=token attribute to use to set Principal name keycloak.realm.principal-attribute=token attribute to use to set Principal name
keycloak.realm.proxy-url=The URL for the HTTP proxy if one is used. keycloak.realm.proxy-url=The URL for the HTTP proxy if one is used.
keycloak.realm.verify-token-audience=If true, then during bearer-only authentication, the adapter will verify if token contains this client name (resource) as an audience
keycloak.secure-deployment=A deployment secured by Keycloak keycloak.secure-deployment=A deployment secured by Keycloak
keycloak.secure-deployment.add=Add a deployment to be secured by Keycloak keycloak.secure-deployment.add=Add a deployment to be secured by Keycloak
@ -83,7 +84,9 @@ keycloak.secure-deployment.principal-attribute=token attribute to use to set Pri
keycloak.secure-deployment.turn-off-change-session-id-on-login=The session id is changed by default on a successful login. Change this to true if you want to turn this off keycloak.secure-deployment.turn-off-change-session-id-on-login=The session id is changed by default on a successful login. Change this to true if you want to turn this off
keycloak.secure-deployment.token-minimum-time-to-live=The adapter will refresh the token if the current token is expired OR will expire in 'token-minimum-time-to-live' seconds or less keycloak.secure-deployment.token-minimum-time-to-live=The adapter will refresh the token if the current token is expired OR will expire in 'token-minimum-time-to-live' seconds or less
keycloak.secure-deployment.min-time-between-jwks-requests=If adapter recognize token signed by unknown public key, it will try to download new public key from keycloak server. However it won't try to download if already tried it in less than 'min-time-between-jwks-requests' seconds keycloak.secure-deployment.min-time-between-jwks-requests=If adapter recognize token signed by unknown public key, it will try to download new public key from keycloak server. However it won't try to download if already tried it in less than 'min-time-between-jwks-requests' seconds
keycloak.secure-deployment.public-key-cache-ttl=Maximum time the downloaded public keys are considered valid. When this time reach, the adapter is forced to download public keys from keycloak server
keycloak.secure-deployment.proxy-url=The URL for the HTTP proxy if one is used. keycloak.secure-deployment.proxy-url=The URL for the HTTP proxy if one is used.
keycloak.secure-deployment.verify-token-audience=If true, then during bearer-only authentication, the adapter will verify if token contains this client name (resource) as an audience
keycloak.secure-deployment.credential=Credential value keycloak.secure-deployment.credential=Credential value
keycloak.credential=Credential keycloak.credential=Credential

View file

@ -66,6 +66,7 @@
<xs:element name="token-store" type="xs:string" minOccurs="0" maxOccurs="1"/> <xs:element name="token-store" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="principal-attribute" type="xs:string" minOccurs="0" maxOccurs="1"/> <xs:element name="principal-attribute" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/> <xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="verify-token-audience" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
</xs:all> </xs:all>
<xs:attribute name="name" type="xs:string" use="required"> <xs:attribute name="name" type="xs:string" use="required">
<xs:annotation> <xs:annotation>
@ -108,7 +109,9 @@
<xs:element name="turn-off-change-session-id-on-login" type="xs:boolean" minOccurs="0" maxOccurs="1" /> <xs:element name="turn-off-change-session-id-on-login" type="xs:boolean" minOccurs="0" maxOccurs="1" />
<xs:element name="token-minimum-time-to-live" type="xs:integer" minOccurs="0" maxOccurs="1"/> <xs:element name="token-minimum-time-to-live" type="xs:integer" minOccurs="0" maxOccurs="1"/>
<xs:element name="min-time-between-jwks-requests" type="xs:integer" minOccurs="0" maxOccurs="1"/> <xs:element name="min-time-between-jwks-requests" type="xs:integer" minOccurs="0" maxOccurs="1"/>
<xs:element name="public-key-cache-ttl" type="xs:integer" minOccurs="0" maxOccurs="1"/>
<xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/> <xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="verify-token-audience" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
</xs:all> </xs:all>
<xs:attribute name="name" type="xs:string" use="required"> <xs:attribute name="name" type="xs:string" use="required">
<xs:annotation> <xs:annotation>

View file

@ -24,16 +24,13 @@ import org.keycloak.OAuthErrorException;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.ServerRequest; import org.keycloak.adapters.ServerRequest;
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.common.VerificationException; import org.keycloak.common.VerificationException;
import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity; import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Form; import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
@ -537,15 +534,9 @@ public class KeycloakInstalled {
refreshToken = tokenResponse.getRefreshToken(); refreshToken = tokenResponse.getRefreshToken();
idTokenString = tokenResponse.getIdToken(); idTokenString = tokenResponse.getIdToken();
token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenString, idTokenString, deployment);
if (idTokenString != null) { token = tokens.getAccessToken();
try { idToken = tokens.getIdToken();
JWSInput input = new JWSInput(idTokenString);
idToken = input.readJsonContent(IDToken.class);
} catch (JWSInputException e) {
throw new VerificationException();
}
}
} }
public AccessToken getToken() { public AccessToken getToken() {

View file

@ -96,6 +96,12 @@ public class SecureDeploymentDefinition extends SimpleResourceDefinition {
.setValidator(new IntRangeValidator(-1, true)) .setValidator(new IntRangeValidator(-1, true))
.setAllowExpression(true) .setAllowExpression(true)
.build(); .build();
protected static final SimpleAttributeDefinition PUBLIC_KEY_CACHE_TTL =
new SimpleAttributeDefinitionBuilder("public-key-cache-ttl", ModelType.INT, true)
.setXmlName("public-key-cache-ttl")
.setAllowExpression(true)
.setValidator(new IntRangeValidator(-1, true))
.build();
protected static final List<SimpleAttributeDefinition> DEPLOYMENT_ONLY_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>(); protected static final List<SimpleAttributeDefinition> DEPLOYMENT_ONLY_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();
static { static {
@ -108,6 +114,7 @@ public class SecureDeploymentDefinition extends SimpleResourceDefinition {
DEPLOYMENT_ONLY_ATTRIBUTES.add(TURN_OFF_CHANGE_SESSION); DEPLOYMENT_ONLY_ATTRIBUTES.add(TURN_OFF_CHANGE_SESSION);
DEPLOYMENT_ONLY_ATTRIBUTES.add(TOKEN_MINIMUM_TIME_TO_LIVE); DEPLOYMENT_ONLY_ATTRIBUTES.add(TOKEN_MINIMUM_TIME_TO_LIVE);
DEPLOYMENT_ONLY_ATTRIBUTES.add(MIN_TIME_BETWEEN_JWKS_REQUESTS); DEPLOYMENT_ONLY_ATTRIBUTES.add(MIN_TIME_BETWEEN_JWKS_REQUESTS);
DEPLOYMENT_ONLY_ATTRIBUTES.add(PUBLIC_KEY_CACHE_TTL);
} }
protected static final List<SimpleAttributeDefinition> ALL_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>(); protected static final List<SimpleAttributeDefinition> ALL_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();

View file

@ -194,6 +194,13 @@ public class SharedAttributeDefinitons {
.setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true))
.build(); .build();
protected static final SimpleAttributeDefinition VERIFY_TOKEN_AUDIENCE =
new SimpleAttributeDefinitionBuilder("verify-token-audience", ModelType.BOOLEAN, true)
.setXmlName("verify-token-audience")
.setAllowExpression(true)
.setDefaultValue(new ModelNode(false))
.build();
protected static final List<SimpleAttributeDefinition> ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>(); protected static final List<SimpleAttributeDefinition> ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();
static { static {
ATTRIBUTES.add(REALM_PUBLIC_KEY); ATTRIBUTES.add(REALM_PUBLIC_KEY);
@ -222,6 +229,7 @@ public class SharedAttributeDefinitons {
ATTRIBUTES.add(AUTODETECT_BEARER_ONLY); ATTRIBUTES.add(AUTODETECT_BEARER_ONLY);
ATTRIBUTES.add(IGNORE_OAUTH_QUERY_PARAMETER); ATTRIBUTES.add(IGNORE_OAUTH_QUERY_PARAMETER);
ATTRIBUTES.add(PROXY_URL); ATTRIBUTES.add(PROXY_URL);
ATTRIBUTES.add(VERIFY_TOKEN_AUDIENCE);
} }
/** /**

View file

@ -50,6 +50,7 @@ keycloak.realm.principal-attribute=token attribute to use to set Principal name
keycloak.realm.autodetect-bearer-only=autodetect bearer-only requests keycloak.realm.autodetect-bearer-only=autodetect bearer-only requests
keycloak.realm.ignore-oauth-query-parameter=disable query parameter parsing for access_token keycloak.realm.ignore-oauth-query-parameter=disable query parameter parsing for access_token
keycloak.realm.proxy-url=The URL for the HTTP proxy if one is used. keycloak.realm.proxy-url=The URL for the HTTP proxy if one is used.
keycloak.realm.verify-token-audience=If true, then during bearer-only authentication, the adapter will verify if token contains this client name (resource) as an audience
keycloak.secure-deployment=A deployment secured by Keycloak keycloak.secure-deployment=A deployment secured by Keycloak
keycloak.secure-deployment.add=Add a deployment to be secured by Keycloak keycloak.secure-deployment.add=Add a deployment to be secured by Keycloak
@ -87,9 +88,11 @@ keycloak.secure-deployment.principal-attribute=token attribute to use to set Pri
keycloak.secure-deployment.turn-off-change-session-id-on-login=The session id is changed by default on a successful login. Change this to true if you want to turn this off keycloak.secure-deployment.turn-off-change-session-id-on-login=The session id is changed by default on a successful login. Change this to true if you want to turn this off
keycloak.secure-deployment.token-minimum-time-to-live=The adapter will refresh the token if the current token is expired OR will expire in 'token-minimum-time-to-live' seconds or less keycloak.secure-deployment.token-minimum-time-to-live=The adapter will refresh the token if the current token is expired OR will expire in 'token-minimum-time-to-live' seconds or less
keycloak.secure-deployment.min-time-between-jwks-requests=If adapter recognize token signed by unknown public key, it will try to download new public key from keycloak server. However it won't try to download if already tried it in less than 'min-time-between-jwks-requests' seconds keycloak.secure-deployment.min-time-between-jwks-requests=If adapter recognize token signed by unknown public key, it will try to download new public key from keycloak server. However it won't try to download if already tried it in less than 'min-time-between-jwks-requests' seconds
keycloak.secure-deployment.public-key-cache-ttl=Maximum time the downloaded public keys are considered valid. When this time reach, the adapter is forced to download public keys from keycloak server
keycloak.secure-deployment.autodetect-bearer-only=autodetect bearer-only requests keycloak.secure-deployment.autodetect-bearer-only=autodetect bearer-only requests
keycloak.secure-deployment.ignore-oauth-query-parameter=disable query parameter parsing for access_token keycloak.secure-deployment.ignore-oauth-query-parameter=disable query parameter parsing for access_token
keycloak.secure-deployment.proxy-url=The URL for the HTTP proxy if one is used. keycloak.secure-deployment.proxy-url=The URL for the HTTP proxy if one is used.
keycloak.secure-deployment.verify-token-audience=If true, then during bearer-only authentication, the adapter will verify if token contains this client name (resource) as an audience
keycloak.secure-deployment.credential=Credential value keycloak.secure-deployment.credential=Credential value
keycloak.credential=Credential keycloak.credential=Credential

View file

@ -69,6 +69,7 @@
<xs:element name="autodetect-bearer-only" type="xs:boolean" minOccurs="0" maxOccurs="1"/> <xs:element name="autodetect-bearer-only" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="ignore-oauth-query-parameter" type="xs:boolean" minOccurs="0" maxOccurs="1"/> <xs:element name="ignore-oauth-query-parameter" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/> <xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="verify-token-audience" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
</xs:all> </xs:all>
<xs:attribute name="name" type="xs:string" use="required"> <xs:attribute name="name" type="xs:string" use="required">
<xs:annotation> <xs:annotation>
@ -112,9 +113,11 @@
<xs:element name="turn-off-change-session-id-on-login" type="xs:boolean" minOccurs="0" maxOccurs="1" /> <xs:element name="turn-off-change-session-id-on-login" type="xs:boolean" minOccurs="0" maxOccurs="1" />
<xs:element name="token-minimum-time-to-live" type="xs:integer" minOccurs="0" maxOccurs="1"/> <xs:element name="token-minimum-time-to-live" type="xs:integer" minOccurs="0" maxOccurs="1"/>
<xs:element name="min-time-between-jwks-requests" type="xs:integer" minOccurs="0" maxOccurs="1"/> <xs:element name="min-time-between-jwks-requests" type="xs:integer" minOccurs="0" maxOccurs="1"/>
<xs:element name="public-key-cache-ttl" type="xs:integer" minOccurs="0" maxOccurs="1"/>
<xs:element name="autodetect-bearer-only" type="xs:boolean" minOccurs="0" maxOccurs="1"/> <xs:element name="autodetect-bearer-only" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="ignore-oauth-query-parameter" type="xs:boolean" minOccurs="0" maxOccurs="1"/> <xs:element name="ignore-oauth-query-parameter" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/> <xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="verify-token-audience" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
</xs:all> </xs:all>
<xs:attribute name="name" type="xs:string" use="required"> <xs:attribute name="name" type="xs:string" use="required">
<xs:annotation> <xs:annotation>

View file

@ -23,12 +23,14 @@
<turn-off-change-session-id-on-login>false</turn-off-change-session-id-on-login> <turn-off-change-session-id-on-login>false</turn-off-change-session-id-on-login>
<token-minimum-time-to-live>10</token-minimum-time-to-live> <token-minimum-time-to-live>10</token-minimum-time-to-live>
<min-time-between-jwks-requests>20</min-time-between-jwks-requests> <min-time-between-jwks-requests>20</min-time-between-jwks-requests>
<public-key-cache-ttl>3600</public-key-cache-ttl>
<realm-public-key> <realm-public-key>
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB
</realm-public-key> </realm-public-key>
<auth-server-url>http://localhost:8080/auth</auth-server-url> <auth-server-url>http://localhost:8080/auth</auth-server-url>
<ssl-required>EXTERNAL</ssl-required> <ssl-required>EXTERNAL</ssl-required>
<proxy-url>http://localhost:9000</proxy-url> <proxy-url>http://localhost:9000</proxy-url>
<verify-token-audience>true</verify-token-audience>
<credential name="secret">0aa31d98-e0aa-404c-b6e0-e771dba1e798</credential> <credential name="secret">0aa31d98-e0aa-404c-b6e0-e771dba1e798</credential>
</secure-deployment> </secure-deployment>
<secure-deployment name="http-endpoint"> <secure-deployment name="http-endpoint">

View file

@ -95,6 +95,12 @@ abstract class AbstractAdapterConfigurationDefinition extends SimpleResourceDefi
.setValidator(new IntRangeValidator(-1, true)) .setValidator(new IntRangeValidator(-1, true))
.setAllowExpression(true) .setAllowExpression(true)
.build(); .build();
protected static final SimpleAttributeDefinition PUBLIC_KEY_CACHE_TTL =
new SimpleAttributeDefinitionBuilder("public-key-cache-ttl", ModelType.INT, true)
.setXmlName("public-key-cache-ttl")
.setAllowExpression(true)
.setValidator(new IntRangeValidator(-1, true))
.build();
static final List<SimpleAttributeDefinition> DEPLOYMENT_ONLY_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>(); static final List<SimpleAttributeDefinition> DEPLOYMENT_ONLY_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();
@ -108,6 +114,7 @@ abstract class AbstractAdapterConfigurationDefinition extends SimpleResourceDefi
DEPLOYMENT_ONLY_ATTRIBUTES.add(TURN_OFF_CHANGE_SESSION); DEPLOYMENT_ONLY_ATTRIBUTES.add(TURN_OFF_CHANGE_SESSION);
DEPLOYMENT_ONLY_ATTRIBUTES.add(TOKEN_MINIMUM_TIME_TO_LIVE); DEPLOYMENT_ONLY_ATTRIBUTES.add(TOKEN_MINIMUM_TIME_TO_LIVE);
DEPLOYMENT_ONLY_ATTRIBUTES.add(MIN_TIME_BETWEEN_JWKS_REQUESTS); DEPLOYMENT_ONLY_ATTRIBUTES.add(MIN_TIME_BETWEEN_JWKS_REQUESTS);
DEPLOYMENT_ONLY_ATTRIBUTES.add(PUBLIC_KEY_CACHE_TTL);
} }
static final List<SimpleAttributeDefinition> ALL_ATTRIBUTES = new ArrayList(); static final List<SimpleAttributeDefinition> ALL_ATTRIBUTES = new ArrayList();

View file

@ -200,6 +200,13 @@ public class SharedAttributeDefinitons {
.setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true))
.build(); .build();
protected static final SimpleAttributeDefinition VERIFY_TOKEN_AUDIENCE =
new SimpleAttributeDefinitionBuilder("verify-token-audience", ModelType.BOOLEAN, true)
.setXmlName("verify-token-audience")
.setAllowExpression(true)
.setDefaultValue(new ModelNode(false))
.build();
protected static final List<SimpleAttributeDefinition> ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>(); protected static final List<SimpleAttributeDefinition> ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();
static { static {
@ -230,6 +237,7 @@ public class SharedAttributeDefinitons {
ATTRIBUTES.add(AUTODETECT_BEARER_ONLY); ATTRIBUTES.add(AUTODETECT_BEARER_ONLY);
ATTRIBUTES.add(IGNORE_OAUTH_QUERY_PARAMETER); ATTRIBUTES.add(IGNORE_OAUTH_QUERY_PARAMETER);
ATTRIBUTES.add(PROXY_URL); ATTRIBUTES.add(PROXY_URL);
ATTRIBUTES.add(VERIFY_TOKEN_AUDIENCE);
} }
private static boolean isSet(ModelNode attributes, SimpleAttributeDefinition def) { private static boolean isSet(ModelNode attributes, SimpleAttributeDefinition def) {

View file

@ -53,6 +53,7 @@ keycloak.realm.principal-attribute=token attribute to use to set Principal name
keycloak.realm.autodetect-bearer-only=autodetect bearer-only requests keycloak.realm.autodetect-bearer-only=autodetect bearer-only requests
keycloak.realm.ignore-oauth-query-parameter=disable query parameter parsing for access_token keycloak.realm.ignore-oauth-query-parameter=disable query parameter parsing for access_token
keycloak.realm.proxy-url=The URL for the HTTP proxy if one is used. keycloak.realm.proxy-url=The URL for the HTTP proxy if one is used.
keycloak.realm.verify-token-audience=If true, then during bearer-only authentication, the adapter will verify if token contains this client name (resource) as an audience
keycloak.secure-deployment=A deployment secured by Keycloak keycloak.secure-deployment=A deployment secured by Keycloak
keycloak.secure-deployment.add=Add a deployment to be secured by Keycloak keycloak.secure-deployment.add=Add a deployment to be secured by Keycloak
@ -93,8 +94,10 @@ keycloak.secure-deployment.principal-attribute=token attribute to use to set Pri
keycloak.secure-deployment.turn-off-change-session-id-on-login=The session id is changed by default on a successful login. Change this to true if you want to turn this off keycloak.secure-deployment.turn-off-change-session-id-on-login=The session id is changed by default on a successful login. Change this to true if you want to turn this off
keycloak.secure-deployment.token-minimum-time-to-live=The adapter will refresh the token if the current token is expired OR will expire in 'token-minimum-time-to-live' seconds or less keycloak.secure-deployment.token-minimum-time-to-live=The adapter will refresh the token if the current token is expired OR will expire in 'token-minimum-time-to-live' seconds or less
keycloak.secure-deployment.min-time-between-jwks-requests=If adapter recognize token signed by unknown public key, it will try to download new public key from keycloak server. However it won't try to download if already tried it in less than 'min-time-between-jwks-requests' seconds keycloak.secure-deployment.min-time-between-jwks-requests=If adapter recognize token signed by unknown public key, it will try to download new public key from keycloak server. However it won't try to download if already tried it in less than 'min-time-between-jwks-requests' seconds
keycloak.secure-deployment.public-key-cache-ttl=Maximum time the downloaded public keys are considered valid. When this time reach, the adapter is forced to download public keys from keycloak server
keycloak.secure-deployment.ignore-oauth-query-parameter=disable query parameter parsing for access_token keycloak.secure-deployment.ignore-oauth-query-parameter=disable query parameter parsing for access_token
keycloak.secure-deployment.proxy-url=The URL for the HTTP proxy if one is used. keycloak.secure-deployment.proxy-url=The URL for the HTTP proxy if one is used.
keycloak.secure-deployment.verify-token-audience=If true, then during bearer-only authentication, the adapter will verify if token contains this client name (resource) as an audience
keycloak.secure-server=A deployment secured by Keycloak keycloak.secure-server=A deployment secured by Keycloak
keycloak.secure-server.add=Add a deployment to be secured by Keycloak keycloak.secure-server.add=Add a deployment to be secured by Keycloak
@ -135,8 +138,10 @@ keycloak.secure-server.principal-attribute=token attribute to use to set Princip
keycloak.secure-server.turn-off-change-session-id-on-login=The session id is changed by default on a successful login. Change this to true if you want to turn this off keycloak.secure-server.turn-off-change-session-id-on-login=The session id is changed by default on a successful login. Change this to true if you want to turn this off
keycloak.secure-server.token-minimum-time-to-live=The adapter will refresh the token if the current token is expired OR will expire in 'token-minimum-time-to-live' seconds or less keycloak.secure-server.token-minimum-time-to-live=The adapter will refresh the token if the current token is expired OR will expire in 'token-minimum-time-to-live' seconds or less
keycloak.secure-server.min-time-between-jwks-requests=If adapter recognize token signed by unknown public key, it will try to download new public key from keycloak server. However it won't try to download if already tried it in less than 'min-time-between-jwks-requests' seconds keycloak.secure-server.min-time-between-jwks-requests=If adapter recognize token signed by unknown public key, it will try to download new public key from keycloak server. However it won't try to download if already tried it in less than 'min-time-between-jwks-requests' seconds
keycloak.secure-server.public-key-cache-ttl=Maximum time the downloaded public keys are considered valid. When this time reach, the adapter is forced to download public keys from keycloak server
keycloak.secure-server.ignore-oauth-query-parameter=disable query parameter parsing for access_token keycloak.secure-server.ignore-oauth-query-parameter=disable query parameter parsing for access_token
keycloak.secure-server.proxy-url=The URL for the HTTP proxy if one is used. keycloak.secure-server.proxy-url=The URL for the HTTP proxy if one is used.
keycloak.secure-server.verify-token-audience=If true, then during bearer-only authentication, the adapter will verify if token contains this client name (resource) as an audience
keycloak.secure-deployment.credential=Credential value keycloak.secure-deployment.credential=Credential value
keycloak.secure-server.credential=Credential value keycloak.secure-server.credential=Credential value

View file

@ -71,6 +71,7 @@
<xs:element name="autodetect-bearer-only" type="xs:boolean" minOccurs="0" maxOccurs="1"/> <xs:element name="autodetect-bearer-only" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="ignore-oauth-query-parameter" type="xs:boolean" minOccurs="0" maxOccurs="1"/> <xs:element name="ignore-oauth-query-parameter" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/> <xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="verify-token-audience" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
</xs:all> </xs:all>
<xs:attribute name="name" type="xs:string" use="required"> <xs:attribute name="name" type="xs:string" use="required">
<xs:annotation> <xs:annotation>
@ -116,9 +117,11 @@
<xs:element name="turn-off-change-session-id-on-login" type="xs:boolean" minOccurs="0" maxOccurs="1" /> <xs:element name="turn-off-change-session-id-on-login" type="xs:boolean" minOccurs="0" maxOccurs="1" />
<xs:element name="token-minimum-time-to-live" type="xs:integer" minOccurs="0" maxOccurs="1"/> <xs:element name="token-minimum-time-to-live" type="xs:integer" minOccurs="0" maxOccurs="1"/>
<xs:element name="min-time-between-jwks-requests" type="xs:integer" minOccurs="0" maxOccurs="1"/> <xs:element name="min-time-between-jwks-requests" type="xs:integer" minOccurs="0" maxOccurs="1"/>
<xs:element name="public-key-cache-ttl" type="xs:integer" minOccurs="0" maxOccurs="1"/>
<xs:element name="autodetect-bearer-only" type="xs:boolean" minOccurs="0" maxOccurs="1"/> <xs:element name="autodetect-bearer-only" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="ignore-oauth-query-parameter" type="xs:boolean" minOccurs="0" maxOccurs="1"/> <xs:element name="ignore-oauth-query-parameter" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/> <xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="verify-token-audience" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
</xs:all> </xs:all>
<xs:attribute name="name" type="xs:string" use="required"> <xs:attribute name="name" type="xs:string" use="required">
<xs:annotation> <xs:annotation>

View file

@ -53,6 +53,7 @@
<turn-off-change-session-id-on-login>false</turn-off-change-session-id-on-login> <turn-off-change-session-id-on-login>false</turn-off-change-session-id-on-login>
<token-minimum-time-to-live>10</token-minimum-time-to-live> <token-minimum-time-to-live>10</token-minimum-time-to-live>
<min-time-between-jwks-requests>20</min-time-between-jwks-requests> <min-time-between-jwks-requests>20</min-time-between-jwks-requests>
<public-key-cache-ttl>3600</public-key-cache-ttl>
<realm-public-key> <realm-public-key>
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB
</realm-public-key> </realm-public-key>
@ -60,6 +61,7 @@
<ssl-required>EXTERNAL</ssl-required> <ssl-required>EXTERNAL</ssl-required>
<confidential-port>443</confidential-port> <confidential-port>443</confidential-port>
<proxy-url>http://localhost:9000</proxy-url> <proxy-url>http://localhost:9000</proxy-url>
<verify-token-audience>true</verify-token-audience>
<credential name="secret">0aa31d98-e0aa-404c-b6e0-e771dba1e798</credential> <credential name="secret">0aa31d98-e0aa-404c-b6e0-e771dba1e798</credential>
<redirect-rewrite-rule name="^/wsmaster/api/(.*)$">api/$1/</redirect-rewrite-rule> <redirect-rewrite-rule name="^/wsmaster/api/(.*)$">api/$1/</redirect-rewrite-rule>
</secure-deployment> </secure-deployment>

View file

@ -133,6 +133,37 @@ public class TokenVerifier<T extends JsonWebToken> {
} }
}; };
public static class AudienceCheck implements Predicate<JsonWebToken> {
private final String expectedAudience;
public AudienceCheck(String expectedAudience) {
this.expectedAudience = expectedAudience;
}
@Override
public boolean test(JsonWebToken t) throws VerificationException {
if (expectedAudience == null) {
throw new VerificationException("Missing expectedAudience");
}
String[] audience = t.getAudience();
if (audience == null) {
throw new VerificationException("No audience in the token");
}
for (String aud : audience) {
if (expectedAudience.equals(aud)) {
return true;
}
}
throw new VerificationException("Expected audience not available in the token");
}
};
private String tokenString; private String tokenString;
private Class<? extends T> clazz; private Class<? extends T> clazz;
private PublicKey publicKey; private PublicKey publicKey;
@ -311,6 +342,16 @@ public class TokenVerifier<T extends JsonWebToken> {
return replaceCheck(RealmUrlCheck.class, this.checkRealmUrl, new RealmUrlCheck(realmUrl)); return replaceCheck(RealmUrlCheck.class, this.checkRealmUrl, new RealmUrlCheck(realmUrl));
} }
/**
* Add check for verifying that token contains the expectedAudience
*
* @param expectedAudience Audience, which needs to be in the target token. Can't be null
* @return This token verifier
*/
public TokenVerifier<T> audience(String expectedAudience) {
return this.replaceCheck(AudienceCheck.class, true, new AudienceCheck(expectedAudience));
}
public TokenVerifier<T> parse() throws VerificationException { public TokenVerifier<T> parse() throws VerificationException {
if (jws == null) { if (jws == null) {
if (tokenString == null) { if (tokenString == null) {

View file

@ -40,7 +40,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
"register-node-at-startup", "register-node-period", "token-store", "principal-attribute", "register-node-at-startup", "register-node-period", "token-store", "principal-attribute",
"proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live", "proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live",
"min-time-between-jwks-requests", "public-key-cache-ttl", "min-time-between-jwks-requests", "public-key-cache-ttl",
"policy-enforcer", "ignore-oauth-query-parameter" "policy-enforcer", "ignore-oauth-query-parameter", "verify-token-audience"
}) })
public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClientConfig { public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClientConfig {
@ -85,6 +85,8 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
protected boolean pkce = false; protected boolean pkce = false;
@JsonProperty("ignore-oauth-query-parameter") @JsonProperty("ignore-oauth-query-parameter")
protected boolean ignoreOAuthQueryParameter = false; protected boolean ignoreOAuthQueryParameter = false;
@JsonProperty("verify-token-audience")
protected boolean verifyTokenAudience = false;
/** /**
* The Proxy url to use for requests to the auth-server, configurable via the adapter config property {@code proxy-url}. * The Proxy url to use for requests to the auth-server, configurable via the adapter config property {@code proxy-url}.
@ -268,4 +270,12 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
public void setIgnoreOAuthQueryParameter(boolean ignoreOAuthQueryParameter) { public void setIgnoreOAuthQueryParameter(boolean ignoreOAuthQueryParameter) {
this.ignoreOAuthQueryParameter = ignoreOAuthQueryParameter; this.ignoreOAuthQueryParameter = ignoreOAuthQueryParameter;
} }
public boolean isVerifyTokenAudience() {
return verifyTokenAudience;
}
public void setVerifyTokenAudience(boolean verifyTokenAudience) {
this.verifyTokenAudience = verifyTokenAudience;
}
} }

View file

@ -248,9 +248,45 @@ public class RSAVerifierTest {
AccessToken v = null; AccessToken v = null;
try { try {
v = verifySkeletonKeyToken(encoded); v = verifySkeletonKeyToken(encoded);
Assert.fail();
} catch (VerificationException ignored) { } catch (VerificationException ignored) {
} }
} }
@Test
public void testAudience() throws Exception {
token.addAudience("my-app");
token.addAudience("your-app");
String encoded = new JWSBuilder()
.jsonContent(token)
.rsa256(idpPair.getPrivate());
verifyAudience(encoded, "my-app");
verifyAudience(encoded, "your-app");
try {
verifyAudience(encoded, "other-app");
Assert.fail();
} catch (VerificationException ignored) {
System.out.println(ignored.getMessage());
}
try {
verifyAudience(encoded, null);
Assert.fail();
} catch (VerificationException ignored) {
System.out.println(ignored.getMessage());
}
}
private void verifyAudience(String encodedToken, String expectedAudience) throws VerificationException {
TokenVerifier.create(encodedToken, AccessToken.class)
.publicKey(idpPair.getPublic())
.realmUrl("http://localhost:8080/auth/realm")
.audience(expectedAudience)
.verify();
}
} }

View file

@ -31,7 +31,7 @@ import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.ServerRequest; import org.keycloak.adapters.ServerRequest;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils; import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.common.VerificationException; import org.keycloak.common.VerificationException;
import org.keycloak.common.util.StreamUtil; import org.keycloak.common.util.StreamUtil;
import org.keycloak.common.util.UriUtils; import org.keycloak.common.util.UriUtils;
@ -43,7 +43,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
@ -152,7 +152,8 @@ public class ProductServiceAccountServlet extends HttpServlet {
private void setTokens(HttpServletRequest req, KeycloakDeployment deployment, AccessTokenResponse tokenResponse) throws IOException, VerificationException { private void setTokens(HttpServletRequest req, KeycloakDeployment deployment, AccessTokenResponse tokenResponse) throws IOException, VerificationException {
String token = tokenResponse.getToken(); String token = tokenResponse.getToken();
String refreshToken = tokenResponse.getRefreshToken(); String refreshToken = tokenResponse.getRefreshToken();
AccessToken tokenParsed = AdapterRSATokenVerifier.verifyToken(token, deployment); AdapterTokenVerifier.VerifiedTokens parsedTokens = AdapterTokenVerifier.verifyTokens(token, tokenResponse.getIdToken(), deployment);
AccessToken tokenParsed = parsedTokens.getAccessToken();
req.getSession().setAttribute(TOKEN, token); req.getSession().setAttribute(TOKEN, token);
req.getSession().setAttribute(REFRESH_TOKEN, refreshToken); req.getSession().setAttribute(REFRESH_TOKEN, refreshToken);
req.getSession().setAttribute(TOKEN_PARSED, tokenParsed); req.getSession().setAttribute(TOKEN_PARSED, tokenParsed);

View file

@ -22,13 +22,17 @@ import org.keycloak.authentication.ClientAuthenticator;
import org.keycloak.authentication.ClientAuthenticatorFactory; import org.keycloak.authentication.ClientAuthenticatorFactory;
import org.keycloak.authorization.admin.AuthorizationService; import org.keycloak.authorization.admin.AuthorizationService;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.ClientInstallationProvider; import org.keycloak.protocol.ClientInstallationProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.ClientManager;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -64,6 +68,10 @@ public class KeycloakOIDCClientInstallation implements ClientInstallationProvide
rep.setCredentials(adapterConfig); rep.setCredentials(adapterConfig);
} }
if (showVerifyTokenAudience(client)) {
rep.setVerifyTokenAudience(true);
}
configureAuthorizationSettings(session, client, rep); configureAuthorizationSettings(session, client, rep);
String json = null; String json = null;
@ -95,6 +103,24 @@ public class KeycloakOIDCClientInstallation implements ClientInstallationProvide
} }
// Check if there is audience client scope created for particular client. If yes, admin wants verifying token audience
static boolean showVerifyTokenAudience(ClientModel client) {
String clientId = client.getClientId();
ClientScopeModel clientScope = KeycloakModelUtils.getClientScopeByName(client.getRealm(), clientId);
if (clientScope == null) {
return false;
}
for (ProtocolMapperModel protocolMapper : clientScope.getProtocolMappers()) {
if (AudienceProtocolMapper.PROVIDER_ID.equals(protocolMapper.getProtocolMapper()) && (clientId.equals(protocolMapper.getConfig().get(AudienceProtocolMapper.INCLUDED_CLIENT_AUDIENCE)))) {
return true;
}
}
return false;
}
@Override @Override
public String getProtocol() { public String getProtocol() {
return OIDCLoginProtocol.LOGIN_PROTOCOL; return OIDCLoginProtocol.LOGIN_PROTOCOL;

View file

@ -49,6 +49,11 @@ public class KeycloakOIDCJbossSubsystemClientInstallation implements ClientInsta
} }
buffer.append(" <ssl-required>").append(realm.getSslRequired().name()).append("</ssl-required>\n"); buffer.append(" <ssl-required>").append(realm.getSslRequired().name()).append("</ssl-required>\n");
buffer.append(" <resource>").append(client.getClientId()).append("</resource>\n"); buffer.append(" <resource>").append(client.getClientId()).append("</resource>\n");
if (KeycloakOIDCClientInstallation.showVerifyTokenAudience(client)) {
buffer.append(" <verify-token-audience>true</verify-token-audience>\n");
}
String cred = client.getSecret(); String cred = client.getSecret();
if (KeycloakOIDCClientInstallation.showClientCredentialsAdapterConfig(client)) { if (KeycloakOIDCClientInstallation.showClientCredentialsAdapterConfig(client)) {
Map<String, Object> adapterConfig = KeycloakOIDCClientInstallation.getClientCredentialsAdapterConfig(session, client); Map<String, Object> adapterConfig = KeycloakOIDCClientInstallation.getClientCredentialsAdapterConfig(session, client);

View file

@ -36,7 +36,7 @@ public class AudienceProtocolMapper extends AbstractOIDCProtocolMapper implement
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>(); private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
private static final String INCLUDED_CLIENT_AUDIENCE = "included.client.audience"; public static final String INCLUDED_CLIENT_AUDIENCE = "included.client.audience";
private static final String INCLUDED_CLIENT_AUDIENCE_LABEL = "included.client.audience.label"; private static final String INCLUDED_CLIENT_AUDIENCE_LABEL = "included.client.audience.label";
private static final String INCLUDED_CLIENT_AUDIENCE_HELP_TEXT = "included.client.audience.tooltip"; private static final String INCLUDED_CLIENT_AUDIENCE_HELP_TEXT = "included.client.audience.tooltip";

View file

@ -210,7 +210,7 @@ public class ClientManager {
} }
@JsonPropertyOrder({"realm", "realm-public-key", "bearer-only", "auth-server-url", "ssl-required", @JsonPropertyOrder({"realm", "realm-public-key", "bearer-only", "auth-server-url", "ssl-required",
"resource", "public-client", "credentials", "resource", "public-client", "verify-token-audience", "credentials",
"use-resource-role-mappings"}) "use-resource-role-mappings"})
public static class InstallationAdapterConfig extends BaseRealmConfig { public static class InstallationAdapterConfig extends BaseRealmConfig {
@JsonProperty("resource") @JsonProperty("resource")
@ -223,6 +223,8 @@ public class ClientManager {
protected Boolean publicClient; protected Boolean publicClient;
@JsonProperty("credentials") @JsonProperty("credentials")
protected Map<String, Object> credentials; protected Map<String, Object> credentials;
@JsonProperty("verify-token-audience")
protected Boolean verifyTokenAudience;
@JsonProperty("policy-enforcer") @JsonProperty("policy-enforcer")
protected PolicyEnforcerConfig enforcerConfig; protected PolicyEnforcerConfig enforcerConfig;
@ -250,6 +252,14 @@ public class ClientManager {
this.credentials = credentials; this.credentials = credentials;
} }
public Boolean getVerifyTokenAudience() {
return verifyTokenAudience;
}
public void setVerifyTokenAudience(Boolean verifyTokenAudience) {
this.verifyTokenAudience = verifyTokenAudience;
}
public Boolean getPublicClient() { public Boolean getPublicClient() {
return publicClient; return publicClient;
} }

View file

@ -74,21 +74,20 @@ public class CustomerServlet extends HttpServlet {
//try { //try {
StringBuilder result = new StringBuilder();
String urlBase = ServletTestUtils.getUrlBase(req); String urlBase = ServletTestUtils.getUrlBase(req);
URL url = new URL(urlBase + "/customer-db/"); // Decide what to call based on the URL suffix
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); String serviceUrl;
conn.setRequestMethod("GET"); if (req.getRequestURI().endsWith("/call-customer-db-audience-required")) {
conn.setRequestProperty(HttpHeaders.AUTHORIZATION, "Bearer " + context.getTokenString()); serviceUrl = urlBase + "/customer-db-audience-required/";
BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); } else {
String line; serviceUrl = urlBase + "/customer-db/";
while ((line = rd.readLine()) != null) {
result.append(line);
} }
rd.close();
String result = invokeService(serviceUrl, context);
resp.setContentType("text/html"); resp.setContentType("text/html");
pw.println(result.toString()); pw.println(result);
pw.flush(); pw.flush();
// //
// Response response = target.request().get(); // Response response = target.request().get();
@ -106,4 +105,28 @@ public class CustomerServlet extends HttpServlet {
// } // }
} }
private String invokeService(String serviceUrl, KeycloakSecurityContext context) throws IOException {
StringBuilder result = new StringBuilder();
URL url = new URL(serviceUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty(HttpHeaders.AUTHORIZATION, "Bearer " + context.getTokenString());
if (conn.getResponseCode() != 200) {
conn.getErrorStream().close();
return "Service returned: " + conn.getResponseCode();
}
BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = rd.readLine()) != null) {
result.append(line);
}
rd.close();
return result.toString();
}
} }

View file

@ -0,0 +1,41 @@
/*
* Copyright 2017 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.adapter.page;
import java.net.URL;
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class CustomerDbAudienceRequired extends AbstractPageWithInjectedUrl {
public static final String DEPLOYMENT_NAME = "customer-db-audience-required";
@ArquillianResource
@OperateOnDeployment(DEPLOYMENT_NAME)
private URL url;
@Override
public URL getInjectedUrl() {
return url;
}
}

View file

@ -44,4 +44,14 @@ public class CustomerPortal extends AbstractPageWithInjectedUrl {
return url + "/logout"; return url + "/logout";
} }
public String callCustomerDbAudienceRequiredUrl(boolean attachAudienceScope) {
String url = this.url + "/call-customer-db-audience-required";
if (attachAudienceScope) {
url = url + "?scope=customer-db-audience-required";
}
return url;
}
} }

View file

@ -0,0 +1,243 @@
/*
* Copyright 2017 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.adapter.jaas;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.ws.rs.core.Response;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.jaas.AbstractKeycloakLoginModule;
import org.keycloak.adapters.jaas.BearerTokenLoginModule;
import org.keycloak.adapters.jaas.DirectAccessGrantsLoginModule;
import org.keycloak.adapters.jaas.RolePrincipal;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.utils.io.IOUtil;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LoginModulesTest extends AbstractKeycloakTest {
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
testRealms.add(IOUtil.loadRealm("/adapter-test/demorealm.json"));
}
@Before
public void generateAudienceClientScope() {
if (ApiUtil.findClientScopeByName(adminClient.realm("demo"), "customer-db-audience-required") != null) {
return;
}
// Generate audience client scope
Response resp = adminClient.realm("demo").clientScopes().generateAudienceClientScope("customer-db-audience-required");
String clientScopeId = ApiUtil.getCreatedId(resp);
resp.close();
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("demo"), "customer-portal");
client.addOptionalClientScope(clientScopeId);
}
@Test
public void testDirectAccessGrantLoginModuleLoginFailed() throws Exception {
LoginContext loginContext = new LoginContext("does-not-matter", null,
createJaasCallbackHandler("bburke@redhat.com", "bad-password"),
createJaasConfigurationForDirectGrant(null));
try {
loginContext.login();
Assert.fail("Not expected to successfully login");
} catch (LoginException le) {
// Ignore
}
}
@Test
public void testDirectAccessGrantLoginModuleLoginSuccess() throws Exception {
oauth.realm("demo");
LoginContext loginContext = directGrantLogin(null);
Subject subject = loginContext.getSubject();
// Assert principals in subject
KeycloakPrincipal principal = subject.getPrincipals(KeycloakPrincipal.class).iterator().next();
Assert.assertEquals("bburke@redhat.com", principal.getKeycloakSecurityContext().getToken().getPreferredUsername());
assertToken(principal.getKeycloakSecurityContext().getTokenString(), true);
Set<RolePrincipal> roles = subject.getPrincipals(RolePrincipal.class);
Assert.assertEquals(1, roles.size());
Assert.assertEquals("user", roles.iterator().next().getName());
// Logout and assert token not valid anymore
loginContext.logout();
assertToken(principal.getKeycloakSecurityContext().getTokenString(), false);
}
@Test
public void testBearerLoginFailedLogin() throws Exception {
oauth.realm("demo");
LoginContext directGrantCtx = directGrantLogin(null);
String accessToken = directGrantCtx.getSubject().getPrincipals(KeycloakPrincipal.class).iterator().next()
.getKeycloakSecurityContext().getTokenString();
LoginContext bearerCtx = new LoginContext("does-not-matter", null,
createJaasCallbackHandler("doesn-not-matter", accessToken),
createJaasConfigurationForBearer());
// Login should fail due insufficient audience in the token
try {
bearerCtx.login();
Assert.fail("Not expected to successfully login");
} catch (LoginException le) {
// Ignore
}
directGrantCtx.logout();
}
@Test
public void testBearerLoginSuccess() throws Exception {
oauth.realm("demo");
LoginContext directGrantCtx = directGrantLogin("customer-db-audience-required");
String accessToken = directGrantCtx.getSubject().getPrincipals(KeycloakPrincipal.class).iterator().next()
.getKeycloakSecurityContext().getTokenString();
LoginContext bearerCtx = new LoginContext("does-not-matter", null,
createJaasCallbackHandler("doesn-not-matter", accessToken),
createJaasConfigurationForBearer());
// Login should be successful
bearerCtx.login();
// Assert subject
Subject subject = bearerCtx.getSubject();
KeycloakPrincipal principal = subject.getPrincipals(KeycloakPrincipal.class).iterator().next();
Assert.assertEquals("bburke@redhat.com", principal.getKeycloakSecurityContext().getToken().getPreferredUsername());
assertToken(principal.getKeycloakSecurityContext().getTokenString(), true);
Set<RolePrincipal> roles = subject.getPrincipals(RolePrincipal.class);
Assert.assertEquals(1, roles.size());
Assert.assertEquals("user", roles.iterator().next().getName());
// Logout
bearerCtx.logout();
directGrantCtx.logout();
}
private LoginContext directGrantLogin(String scope) throws LoginException {
LoginContext loginContext = new LoginContext("does-not-matter", null,
createJaasCallbackHandler("bburke@redhat.com", "password"),
createJaasConfigurationForDirectGrant(scope));
loginContext.login();
return loginContext;
}
private void assertToken(String accessToken, boolean expectActive) throws IOException {
String introspectionResponse = oauth.introspectAccessTokenWithClientCredential("customer-portal", "password", accessToken);
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(introspectionResponse);
Assert.assertEquals(expectActive, jsonNode.get("active").asBoolean());
}
private CallbackHandler createJaasCallbackHandler(final String principal, final String password) {
return new CallbackHandler() {
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (Callback callback : callbacks) {
if (callback instanceof NameCallback) {
NameCallback nameCallback = (NameCallback) callback;
nameCallback.setName(principal);
} else if (callback instanceof PasswordCallback) {
PasswordCallback passwordCallback = (PasswordCallback) callback;
passwordCallback.setPassword(password.toCharArray());
} else {
throw new UnsupportedCallbackException(callback, "Unsupported callback: " + callback.getClass().getCanonicalName());
}
}
}
};
}
private Configuration createJaasConfigurationForDirectGrant(String scope) {
return new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, Object> options = new HashMap<>();
options.put(AbstractKeycloakLoginModule.KEYCLOAK_CONFIG_FILE_OPTION, "classpath:adapter-test/customer-portal/WEB-INF/keycloak.json");
if (scope != null) {
options.put(DirectAccessGrantsLoginModule.SCOPE_OPTION, scope);
}
AppConfigurationEntry LMConfiguration = new AppConfigurationEntry(DirectAccessGrantsLoginModule.class.getName(), AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
return new AppConfigurationEntry[] { LMConfiguration };
}
};
}
private Configuration createJaasConfigurationForBearer() {
return new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, Object> options = new HashMap<>();
options.put(AbstractKeycloakLoginModule.KEYCLOAK_CONFIG_FILE_OPTION, "classpath:adapter-test/customer-db-audience-required/WEB-INF/keycloak.json");
AppConfigurationEntry LMConfiguration = new AppConfigurationEntry(BearerTokenLoginModule.class.getName(), AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
return new AppConfigurationEntry[] { LMConfiguration };
}
};
}
}

View file

@ -76,6 +76,7 @@ import org.keycloak.testsuite.adapter.page.BasicAuth;
import org.keycloak.testsuite.adapter.page.ClientSecretJwtSecurePortal; import org.keycloak.testsuite.adapter.page.ClientSecretJwtSecurePortal;
import org.keycloak.testsuite.adapter.page.CustomerCookiePortal; import org.keycloak.testsuite.adapter.page.CustomerCookiePortal;
import org.keycloak.testsuite.adapter.page.CustomerDb; import org.keycloak.testsuite.adapter.page.CustomerDb;
import org.keycloak.testsuite.adapter.page.CustomerDbAudienceRequired;
import org.keycloak.testsuite.adapter.page.CustomerDbErrorPage; import org.keycloak.testsuite.adapter.page.CustomerDbErrorPage;
import org.keycloak.testsuite.adapter.page.CustomerPortal; import org.keycloak.testsuite.adapter.page.CustomerPortal;
import org.keycloak.testsuite.adapter.page.CustomerPortalNoConf; import org.keycloak.testsuite.adapter.page.CustomerPortalNoConf;
@ -213,6 +214,11 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
return servletDeployment(CustomerDb.DEPLOYMENT_NAME, AdapterActionsFilter.class, CustomerDatabaseServlet.class); return servletDeployment(CustomerDb.DEPLOYMENT_NAME, AdapterActionsFilter.class, CustomerDatabaseServlet.class);
} }
@Deployment(name = CustomerDbAudienceRequired.DEPLOYMENT_NAME)
protected static WebArchive customerDbAudienceRequired() {
return servletDeployment(CustomerDbAudienceRequired.DEPLOYMENT_NAME, AdapterActionsFilter.class, CustomerDatabaseServlet.class);
}
@Deployment(name = CustomerDbErrorPage.DEPLOYMENT_NAME) @Deployment(name = CustomerDbErrorPage.DEPLOYMENT_NAME)
protected static WebArchive customerDbErrorPage() { protected static WebArchive customerDbErrorPage() {
return servletDeployment(CustomerDbErrorPage.DEPLOYMENT_NAME, CustomerDatabaseServlet.class, ErrorServlet.class); return servletDeployment(CustomerDbErrorPage.DEPLOYMENT_NAME, CustomerDatabaseServlet.class, ErrorServlet.class);
@ -836,6 +842,50 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
} }
} }
@Test
public void testVerifyTokenAudience() {
// Generate audience client scope
Response resp = adminClient.realm("demo").clientScopes().generateAudienceClientScope("customer-db-audience-required");
String clientScopeId = ApiUtil.getCreatedId(resp);
resp.close();
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("demo"), "customer-portal");
client.addOptionalClientScope(clientScopeId);
// Login without audience scope. Invoke service should end with failure
driver.navigate().to(customerPortal.callCustomerDbAudienceRequiredUrl(false));
assertTrue(testRealmLoginPage.form().isUsernamePresent());
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
testRealmLoginPage.form().login("bburke@redhat.com", "password");
assertCurrentUrlEquals(customerPortal.callCustomerDbAudienceRequiredUrl(false));
String pageSource = driver.getPageSource();
Assert.assertTrue(pageSource.contains("Service returned: 401"));
Assert.assertFalse(pageSource.contains("Stian Thorgersen"));
// Logout TODO: will be good to not request logout to force adapter to use additional scope (and other request parameters)
driver.navigate().to(customerPortal.logout());
waitForPageToLoad();
// Login with requested audience
driver.navigate().to(customerPortal.callCustomerDbAudienceRequiredUrl(true));
assertTrue(testRealmLoginPage.form().isUsernamePresent());
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
testRealmLoginPage.form().login("bburke@redhat.com", "password");
assertCurrentUrlEquals(customerPortal.callCustomerDbAudienceRequiredUrl(false));
pageSource = driver.getPageSource();
Assert.assertFalse(pageSource.contains("Service returned: 401"));
assertLogged();
// logout
String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
.queryParam(OAuth2Constants.REDIRECT_URI, customerPortal.toString()).build("demo").toString();
driver.navigate().to(logoutUri);
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
}
@Test @Test
public void testBasicAuth() { public void testBasicAuth() {
String value = "hello"; String value = "hello";

View file

@ -17,14 +17,20 @@
package org.keycloak.testsuite.admin.client; package org.keycloak.testsuite.admin.client;
import javax.ws.rs.core.Response;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.testsuite.ProfileAssume; import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.util.AdminEventPaths;
import org.keycloak.testsuite.util.WaitUtils;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
@ -97,6 +103,27 @@ public class InstallationTest extends AbstractClientTest {
assertThat(json, containsString("bearer-only")); assertThat(json, containsString("bearer-only"));
assertThat(json, not(containsString("public-client"))); assertThat(json, not(containsString("public-client")));
assertThat(json, not(containsString("credentials"))); assertThat(json, not(containsString("credentials")));
assertThat(json, not(containsString("verify-token-audience")));
}
@Test
public void testOidcBearerOnlyJsonWithAudienceClientScope() {
// Generate audience client scope
Response resp = testRealmResource().clientScopes().generateAudienceClientScope(OIDC_NAME_BEARER_ONLY_NAME);
String clientScopeId = ApiUtil.getCreatedId(resp);
resp.close();
assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientScopeGenerateAudienceClientScopePath(), null, ResourceType.CLIENT_SCOPE);
String json = oidcBearerOnlyClient.getInstallationProvider("keycloak-oidc-keycloak-json");
assertOidcInstallationConfig(json);
assertThat(json, containsString("bearer-only"));
assertThat(json, not(containsString("public-client")));
assertThat(json, not(containsString("credentials")));
assertThat(json, containsString("verify-token-audience"));
// Remove clientScope
testRealmResource().clientScopes().get(clientScopeId).remove();
assertAdminEvents.assertEvent(getRealmId(), OperationType.DELETE, AdminEventPaths.clientScopeResourcePath(clientScopeId), null, ResourceType.CLIENT_SCOPE);
} }
@Test @Test

View file

@ -170,6 +170,11 @@ public class AdminEventPaths {
return uri.toString(); return uri.toString();
} }
public static String clientScopeGenerateAudienceClientScopePath() {
URI uri = UriBuilder.fromUri("").path(RealmResource.class, "clientScopes").path(ClientScopesResource.class, "generateAudienceClientScope").build();
return uri.toString();
}
public static String clientScopeRoleMappingsRealmLevelPath(String clientScopeDbId) { public static String clientScopeRoleMappingsRealmLevelPath(String clientScopeDbId) {
URI uri = UriBuilder.fromUri(clientScopeResourcePath(clientScopeDbId)).path(ClientScopeResource.class, "getScopeMappings") URI uri = UriBuilder.fromUri(clientScopeResourcePath(clientScopeDbId)).path(ClientScopeResource.class, "getScopeMappings")
.path(RoleMappingResource.class, "realmLevel") .path(RoleMappingResource.class, "realmLevel")

View file

@ -0,0 +1,20 @@
<!--
~ 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.
-->
<Context path="/customer-portal">
<Valve className="org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve"/>
</Context>

View file

@ -0,0 +1,46 @@
<?xml version="1.0"?>
<!--
~ 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.
-->
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
<Get name="securityHandler">
<Set name="authenticator">
<New class="org.keycloak.adapters.jetty.KeycloakJettyAuthenticator">
<!--
<Set name="adapterConfig">
<New class="org.keycloak.representations.adapters.config.AdapterConfig">
<Set name="realm">tomcat</Set>
<Set name="resource">customer-portal</Set>
<Set name="authServerUrl">http://localhost:8180/auth</Set>
<Set name="sslRequired">external</Set>
<Set name="credentials">
<Map>
<Entry>
<Item>secret</Item>
<Item>password</Item>
</Entry>
</Map>
</Set>
<Set name="realmKey">MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</Set>
</New>
</Set>
-->
</New>
</Set>
</Get>
</Configure>

View file

@ -0,0 +1,11 @@
{
"realm" : "demo",
"resource" : "customer-db-audience-required",
"auth-server-url": "http://localhost:8180/auth",
"ssl-required" : "external",
"bearer-only" : true,
"enable-cors" : true,
"public-key-cache-ttl": 600,
"verify-token-audience": true
}

View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<module-name>customer-db</module-name>
<filter>
<filter-name>AdapterActionsFilter</filter-name>
<filter-class>org.keycloak.testsuite.adapter.filter.AdapterActionsFilter</filter-class>
</filter>
<servlet>
<servlet-name>Servlet</servlet-name>
<servlet-class>org.keycloak.testsuite.adapter.servlet.CustomerDatabaseServlet</servlet-class>
</servlet>
<filter-mapping>
<filter-name>AdapterActionsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet-mapping>
<servlet-name>Servlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Unsecured</web-resource-name>
<url-pattern>/unsecured/*</url-pattern>
</web-resource-collection>
</security-constraint>
<login-config>
<auth-method>KEYCLOAK</auth-method>
<realm-name>demo</realm-name>
</login-config>
<security-role>
<role-name>admin</role-name>
</security-role>
<security-role>
<role-name>user</role-name>
</security-role>
</web-app>

View file

@ -1,7 +1,7 @@
{ {
"realm": "demo", "realm": "demo",
"resource": "customer-portal", "resource": "customer-portal",
"auth-server-url": "http://localhostt:8180/auth", "auth-server-url": "http://localhost:8180/auth",
"ssl-required" : "external", "ssl-required" : "external",
"expose-token": true, "expose-token": true,
"min-time-between-jwks-requests": 120, "min-time-between-jwks-requests": 120,

View file

@ -133,6 +133,13 @@
"baseUrl": "/customer-db", "baseUrl": "/customer-db",
"bearerOnly": true "bearerOnly": true
}, },
{
"clientId": "customer-db-audience-required",
"enabled": true,
"adminUrl": "/customer-db-audience-required",
"baseUrl": "/customer-db-audience-required",
"bearerOnly": true
},
{ {
"clientId": "customer-portal", "clientId": "customer-portal",
"enabled": true, "enabled": true,