diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java index 701cad872f..8192e71171 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java @@ -482,6 +482,16 @@ public class AdapterDeploymentContext { public void setPublicKeyCacheTtl(int 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) { diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java index 966f8c0ee7..eeda3e3429 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java @@ -18,7 +18,7 @@ package org.keycloak.adapters; 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.AuthOutcome; import org.keycloak.adapters.spi.HttpFacade; @@ -96,7 +96,7 @@ public class BearerTokenRequestAuthenticator { } } try { - token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); + token = AdapterTokenVerifier.verifyToken(tokenString, deployment); } catch (VerificationException e) { log.error("Failed to verify token", e); challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, "invalid_token", e.getMessage()); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java index 7d67dd6cd3..1665c92509 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java @@ -19,7 +19,8 @@ package org.keycloak.adapters; import org.jboss.logging.Logger; 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.common.VerificationException; import org.keycloak.common.util.KeycloakUriBuilder; @@ -71,7 +72,11 @@ public class CookieTokenStore { try { // 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 tokenVerifier = AdapterTokenVerifier.createVerifier(accessTokenString, deployment, true, AccessToken.class) + .checkActive(false) + .verify(); + AccessToken accessToken = tokenVerifier.getToken(); + IDToken idToken; if (idTokenString != null && idTokenString.length() > 0) { try { diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java index e1fce5b7fb..89f38cc593 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java @@ -92,10 +92,11 @@ public class KeycloakDeployment { // https://tools.ietf.org/html/rfc7636 protected boolean pkce = false; protected boolean ignoreOAuthQueryParameter; - + protected Map redirectRewriteRules; protected boolean delegateBearerErrorResponseSending = false; + protected boolean verifyTokenAudience = false; public KeycloakDeployment() { } @@ -477,4 +478,12 @@ public class KeycloakDeployment { public void setDelegateBearerErrorResponseSending(boolean delegateBearerErrorResponseSending) { this.delegateBearerErrorResponseSending = delegateBearerErrorResponseSending; } + + public boolean isVerifyTokenAudience() { + return verifyTokenAudience; + } + + public void setVerifyTokenAudience(boolean verifyTokenAudience) { + this.verifyTokenAudience = verifyTokenAudience; + } } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java index fa7da95bf1..936c065e0d 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java @@ -122,6 +122,7 @@ public class KeycloakDeploymentBuilder { deployment.setPublicKeyCacheTtl(adapterConfig.getPublicKeyCacheTtl()); deployment.setIgnoreOAuthQueryParameter(adapterConfig.isIgnoreOAuthQueryParameter()); deployment.setRewriteRedirectRules(adapterConfig.getRedirectRewriteRules()); + deployment.setVerifyTokenAudience(adapterConfig.isVerifyTokenAudience()); 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"); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java index 2538edb7cb..fb36c4e2f0 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java @@ -19,7 +19,7 @@ package org.keycloak.adapters; import org.jboss.logging.Logger; 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.AuthChallenge; import org.keycloak.adapters.spi.AuthOutcome; @@ -40,7 +40,6 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Map; -import java.util.logging.Level; /** @@ -359,15 +358,9 @@ public class OAuthRequestAuthenticator { } try { - token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); - if (idTokenString != null) { - try { - JWSInput input = new JWSInput(idTokenString); - idToken = input.readJsonContent(IDToken.class); - } catch (JWSInputException e) { - throw new VerificationException(); - } - } + AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenString, idTokenString, deployment); + token = tokens.getAccessToken(); + idToken = tokens.getIdToken(); log.debug("Token Verification succeeded!"); } catch (VerificationException e) { log.error("failed verification of token: " + e.getMessage()); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java index b4d017bc57..0e8b825d35 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java @@ -20,30 +20,27 @@ package org.keycloak.adapters; import java.security.PublicKey; import org.jboss.logging.Logger; +import org.keycloak.TokenVerifier; import org.keycloak.adapters.authentication.ClientCredentialsProvider; 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.UserSessionManagement; +import org.keycloak.common.VerificationException; import org.keycloak.common.util.StreamUtil; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JWK; 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.constants.AdapterConstants; 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.LogoutAction; import org.keycloak.representations.adapters.action.PushNotBeforeAction; import org.keycloak.representations.adapters.action.TestAvailabilityAction; import org.keycloak.util.JsonSerialization; -import java.security.PublicKey; - /** * @author Bill Burke * @version $Revision: 1 $ @@ -213,17 +210,19 @@ public class PreAuthActionsHandler { } try { - JWSInput input = new JWSInput(token); - PublicKey publicKey = AdapterRSATokenVerifier.getPublicKey(input.getHeader().getKeyId(), deployment); - if (RSAProvider.verify(input, publicKey)) { - return input; + // Check just signature. Other things checked in validateAction + TokenVerifier tokenVerifier = AdapterTokenVerifier.createVerifier(token, deployment, false, JsonWebToken.class); + tokenVerifier.verify(); + 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, "no token"); - return null; + facade.getResponse().sendError(403, "token failed verification"); + return null; + } } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java index fa1cefdff5..81d096635e 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java @@ -20,7 +20,7 @@ package org.keycloak.adapters; import org.jboss.logging.Logger; import org.keycloak.AuthorizationContext; 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.util.Time; import org.keycloak.representations.AccessToken; @@ -130,7 +130,8 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext String tokenString = response.getToken(); AccessToken token = null; try { - token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); + AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenString, response.getIdToken(), deployment); + token = tokens.getAccessToken(); log.debug("Token Verification succeeded!"); } catch (VerificationException e) { log.error("failed verification of token"); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java index e7211a9a73..07409f3ab2 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java @@ -27,7 +27,7 @@ import org.jboss.logging.Logger; import org.keycloak.KeycloakSecurityContext; import org.keycloak.adapters.KeycloakDeployment; 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.authorization.client.AuthorizationDeniedException; import org.keycloak.authorization.client.AuthzClient; @@ -171,7 +171,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { } if (authzResponse != null) { - return AdapterRSATokenVerifier.verifyToken(authzResponse.getToken(), deployment); + return AdapterTokenVerifier.verifyToken(authzResponse.getToken(), deployment); } } catch (AuthorizationDeniedException ignore) { LOGGER.debug("Authorization denied", ignore); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java index 28ef0060b1..1c27d6e61c 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java @@ -23,7 +23,7 @@ import org.keycloak.adapters.AdapterUtils; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; 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.util.FindFile; import org.keycloak.common.util.reflections.Reflections; @@ -202,8 +202,16 @@ public abstract class AbstractKeycloakLoginModule implements LoginModule { 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; if (deployment.isUseResourceRoleMappings()) { verifyCaller = token.isVerifyCaller(deployment.getResourceName()); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java index fffd39bad0..b39ff85920 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java @@ -27,6 +27,7 @@ import org.apache.http.message.BasicNameValuePair; import org.jboss.logging.Logger; import org.keycloak.OAuth2Constants; import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils; +import org.keycloak.adapters.rotation.AdapterTokenVerifier; import org.keycloak.common.VerificationException; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.constants.ServiceUrlConstants; @@ -56,11 +57,15 @@ public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule { private static final Logger log = Logger.getLogger(DirectAccessGrantsLoginModule.class); + public static final String SCOPE_OPTION = "scope"; + private String refreshToken; + private String scope; @Override public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { super.initialize(subject, callbackHandler, sharedState, options); + this.scope = (String)options.get(SCOPE_OPTION); // This is used just for logout Iterator 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("password", password)); + if (scope != null) { + formparams.add(new BasicNameValuePair(OAuth2Constants.SCOPE, scope)); + } + ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams); 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 = tokenResponse.getRefreshToken(); - return bearerAuth(tokenResponse.getToken()); + AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenResponse.getToken(), tokenResponse.getIdToken(), deployment); + return postTokenVerification(tokenResponse.getToken(), tokens.getAccessToken()); } @Override diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/AdapterRSATokenVerifier.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/AdapterRSATokenVerifier.java deleted file mode 100644 index 2faa84ab26..0000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/AdapterRSATokenVerifier.java +++ /dev/null @@ -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 Marek Posolda - */ -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(); - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/AdapterTokenVerifier.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/AdapterTokenVerifier.java new file mode 100644 index 0000000000..ab5b69d43f --- /dev/null +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/AdapterTokenVerifier.java @@ -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 Marek Posolda + */ +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 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 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 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 + * @return tokenVerifier + * @throws VerificationException + */ + public static TokenVerifier createVerifier(String tokenString, KeycloakDeployment deployment, boolean withDefaultChecks, Class tokenClass) throws VerificationException { + TokenVerifier 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; + } + } +} diff --git a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java index a30115f6d1..770876b31a 100644 --- a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java +++ b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java @@ -77,6 +77,7 @@ public class KeycloakDeploymentBuilderTest { assertEquals(20, deployment.getMinTimeBetweenJwksRequests()); assertEquals(120, deployment.getPublicKeyCacheTtl()); assertEquals("/api/$1", deployment.getRedirectRewriteRules().get("^/wsmaster/api/(.*)$")); + assertTrue(deployment.isVerifyTokenAudience()); } @Test diff --git a/adapters/oidc/adapter-core/src/test/resources/keycloak.json b/adapters/oidc/adapter-core/src/test/resources/keycloak.json index e1b88816f4..9a7dd22c49 100644 --- a/adapters/oidc/adapter-core/src/test/resources/keycloak.json +++ b/adapters/oidc/adapter-core/src/test/resources/keycloak.json @@ -34,6 +34,7 @@ "min-time-between-jwks-requests": 20, "public-key-cache-ttl": 120, "ignore-oauth-query-parameter": true, + "verify-token-audience": true, "redirect-rewrite-rules" : { "^/wsmaster/api/(.*)$" : "/api/$1" } diff --git a/adapters/oidc/as7-eap6/as7-subsystem/src/main/java/org/keycloak/subsystem/as7/SecureDeploymentDefinition.java b/adapters/oidc/as7-eap6/as7-subsystem/src/main/java/org/keycloak/subsystem/as7/SecureDeploymentDefinition.java index 367fde7feb..dc7b3c6dfa 100755 --- a/adapters/oidc/as7-eap6/as7-subsystem/src/main/java/org/keycloak/subsystem/as7/SecureDeploymentDefinition.java +++ b/adapters/oidc/as7-eap6/as7-subsystem/src/main/java/org/keycloak/subsystem/as7/SecureDeploymentDefinition.java @@ -98,6 +98,12 @@ class SecureDeploymentDefinition extends SimpleResourceDefinition { .setValidator(new IntRangeValidator(-1, true)) .setAllowExpression(true) .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 DEPLOYMENT_ONLY_ATTRIBUTES = new ArrayList(); static { @@ -110,6 +116,7 @@ class SecureDeploymentDefinition extends SimpleResourceDefinition { DEPLOYMENT_ONLY_ATTRIBUTES.add(TURN_OFF_CHANGE_SESSION); DEPLOYMENT_ONLY_ATTRIBUTES.add(TOKEN_MINIMUM_TIME_TO_LIVE); DEPLOYMENT_ONLY_ATTRIBUTES.add(MIN_TIME_BETWEEN_JWKS_REQUESTS); + DEPLOYMENT_ONLY_ATTRIBUTES.add(PUBLIC_KEY_CACHE_TTL); } protected static final List ALL_ATTRIBUTES = new ArrayList(); diff --git a/adapters/oidc/as7-eap6/as7-subsystem/src/main/java/org/keycloak/subsystem/as7/SharedAttributeDefinitons.java b/adapters/oidc/as7-eap6/as7-subsystem/src/main/java/org/keycloak/subsystem/as7/SharedAttributeDefinitons.java index 697d2a86c4..f4dfdd1ba3 100755 --- a/adapters/oidc/as7-eap6/as7-subsystem/src/main/java/org/keycloak/subsystem/as7/SharedAttributeDefinitons.java +++ b/adapters/oidc/as7-eap6/as7-subsystem/src/main/java/org/keycloak/subsystem/as7/SharedAttributeDefinitons.java @@ -173,6 +173,13 @@ class SharedAttributeDefinitons { .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) .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 ATTRIBUTES = new ArrayList(); @@ -200,6 +207,7 @@ class SharedAttributeDefinitons { ATTRIBUTES.add(TOKEN_STORE); ATTRIBUTES.add(PRINCIPAL_ATTRIBUTE); ATTRIBUTES.add(PROXY_URL); + ATTRIBUTES.add(VERIFY_TOKEN_AUDIENCE); } /** diff --git a/adapters/oidc/as7-eap6/as7-subsystem/src/main/resources/org/keycloak/subsystem/as7/LocalDescriptions.properties b/adapters/oidc/as7-eap6/as7-subsystem/src/main/resources/org/keycloak/subsystem/as7/LocalDescriptions.properties index f78d928f08..ca01ed36dc 100755 --- a/adapters/oidc/as7-eap6/as7-subsystem/src/main/resources/org/keycloak/subsystem/as7/LocalDescriptions.properties +++ b/adapters/oidc/as7-eap6/as7-subsystem/src/main/resources/org/keycloak/subsystem/as7/LocalDescriptions.properties @@ -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.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.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.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.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.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.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.credential=Credential diff --git a/adapters/oidc/as7-eap6/as7-subsystem/src/main/resources/schema/keycloak_1_1.xsd b/adapters/oidc/as7-eap6/as7-subsystem/src/main/resources/schema/keycloak_1_1.xsd index 947259781e..0aee4225c0 100755 --- a/adapters/oidc/as7-eap6/as7-subsystem/src/main/resources/schema/keycloak_1_1.xsd +++ b/adapters/oidc/as7-eap6/as7-subsystem/src/main/resources/schema/keycloak_1_1.xsd @@ -66,6 +66,7 @@ + @@ -108,7 +109,9 @@ + + diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java index 501166722b..2472dfe193 100644 --- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java +++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java @@ -24,16 +24,13 @@ import org.keycloak.OAuthErrorException; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; 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.util.KeycloakUriBuilder; -import org.keycloak.jose.jws.JWSInput; -import org.keycloak.jose.jws.JWSInputException; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; -import javax.ws.rs.client.Client; import javax.ws.rs.client.Entity; import javax.ws.rs.core.Form; import javax.ws.rs.core.HttpHeaders; @@ -537,15 +534,9 @@ public class KeycloakInstalled { refreshToken = tokenResponse.getRefreshToken(); idTokenString = tokenResponse.getIdToken(); - token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); - if (idTokenString != null) { - try { - JWSInput input = new JWSInput(idTokenString); - idToken = input.readJsonContent(IDToken.class); - } catch (JWSInputException e) { - throw new VerificationException(); - } - } + AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenString, idTokenString, deployment); + token = tokens.getAccessToken(); + idToken = tokens.getIdToken(); } public AccessToken getToken() { diff --git a/adapters/oidc/wildfly/wf8-subsystem/src/main/java/org/keycloak/subsystem/wf8/extension/SecureDeploymentDefinition.java b/adapters/oidc/wildfly/wf8-subsystem/src/main/java/org/keycloak/subsystem/wf8/extension/SecureDeploymentDefinition.java index bf9dd290b1..1eb783dfd9 100755 --- a/adapters/oidc/wildfly/wf8-subsystem/src/main/java/org/keycloak/subsystem/wf8/extension/SecureDeploymentDefinition.java +++ b/adapters/oidc/wildfly/wf8-subsystem/src/main/java/org/keycloak/subsystem/wf8/extension/SecureDeploymentDefinition.java @@ -96,6 +96,12 @@ public class SecureDeploymentDefinition extends SimpleResourceDefinition { .setValidator(new IntRangeValidator(-1, true)) .setAllowExpression(true) .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 DEPLOYMENT_ONLY_ATTRIBUTES = new ArrayList(); static { @@ -108,6 +114,7 @@ public class SecureDeploymentDefinition extends SimpleResourceDefinition { DEPLOYMENT_ONLY_ATTRIBUTES.add(TURN_OFF_CHANGE_SESSION); DEPLOYMENT_ONLY_ATTRIBUTES.add(TOKEN_MINIMUM_TIME_TO_LIVE); DEPLOYMENT_ONLY_ATTRIBUTES.add(MIN_TIME_BETWEEN_JWKS_REQUESTS); + DEPLOYMENT_ONLY_ATTRIBUTES.add(PUBLIC_KEY_CACHE_TTL); } protected static final List ALL_ATTRIBUTES = new ArrayList(); diff --git a/adapters/oidc/wildfly/wf8-subsystem/src/main/java/org/keycloak/subsystem/wf8/extension/SharedAttributeDefinitons.java b/adapters/oidc/wildfly/wf8-subsystem/src/main/java/org/keycloak/subsystem/wf8/extension/SharedAttributeDefinitons.java index 0751da8cf7..94180e89a6 100755 --- a/adapters/oidc/wildfly/wf8-subsystem/src/main/java/org/keycloak/subsystem/wf8/extension/SharedAttributeDefinitons.java +++ b/adapters/oidc/wildfly/wf8-subsystem/src/main/java/org/keycloak/subsystem/wf8/extension/SharedAttributeDefinitons.java @@ -194,6 +194,13 @@ public class SharedAttributeDefinitons { .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) .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 ATTRIBUTES = new ArrayList(); static { ATTRIBUTES.add(REALM_PUBLIC_KEY); @@ -222,6 +229,7 @@ public class SharedAttributeDefinitons { ATTRIBUTES.add(AUTODETECT_BEARER_ONLY); ATTRIBUTES.add(IGNORE_OAUTH_QUERY_PARAMETER); ATTRIBUTES.add(PROXY_URL); + ATTRIBUTES.add(VERIFY_TOKEN_AUDIENCE); } /** diff --git a/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/org/keycloak/subsystem/wf8/extension/LocalDescriptions.properties b/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/org/keycloak/subsystem/wf8/extension/LocalDescriptions.properties index 30dd04f269..e14c718807 100755 --- a/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/org/keycloak/subsystem/wf8/extension/LocalDescriptions.properties +++ b/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/org/keycloak/subsystem/wf8/extension/LocalDescriptions.properties @@ -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.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.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.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.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.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.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.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.credential=Credential diff --git a/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd b/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd index ebdb6d9b24..4d00ed6384 100755 --- a/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd +++ b/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd @@ -69,6 +69,7 @@ + @@ -112,9 +113,11 @@ + + diff --git a/adapters/oidc/wildfly/wf8-subsystem/src/test/resources/org/keycloak/subsystem/wf8/extension/keycloak-1.1.xml b/adapters/oidc/wildfly/wf8-subsystem/src/test/resources/org/keycloak/subsystem/wf8/extension/keycloak-1.1.xml index 3cc3f20aba..2315ea02ad 100755 --- a/adapters/oidc/wildfly/wf8-subsystem/src/test/resources/org/keycloak/subsystem/wf8/extension/keycloak-1.1.xml +++ b/adapters/oidc/wildfly/wf8-subsystem/src/test/resources/org/keycloak/subsystem/wf8/extension/keycloak-1.1.xml @@ -23,12 +23,14 @@ false 10 20 + 3600 MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB http://localhost:8080/auth EXTERNAL http://localhost:9000 + true 0aa31d98-e0aa-404c-b6e0-e771dba1e798 diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationDefinition.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationDefinition.java index 613c946f4f..bcfd399365 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationDefinition.java +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationDefinition.java @@ -95,6 +95,12 @@ abstract class AbstractAdapterConfigurationDefinition extends SimpleResourceDefi .setValidator(new IntRangeValidator(-1, true)) .setAllowExpression(true) .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 DEPLOYMENT_ONLY_ATTRIBUTES = new ArrayList(); @@ -108,6 +114,7 @@ abstract class AbstractAdapterConfigurationDefinition extends SimpleResourceDefi DEPLOYMENT_ONLY_ATTRIBUTES.add(TURN_OFF_CHANGE_SESSION); DEPLOYMENT_ONLY_ATTRIBUTES.add(TOKEN_MINIMUM_TIME_TO_LIVE); DEPLOYMENT_ONLY_ATTRIBUTES.add(MIN_TIME_BETWEEN_JWKS_REQUESTS); + DEPLOYMENT_ONLY_ATTRIBUTES.add(PUBLIC_KEY_CACHE_TTL); } static final List ALL_ATTRIBUTES = new ArrayList(); diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SharedAttributeDefinitons.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SharedAttributeDefinitons.java index 281e0a9206..1366cb8247 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SharedAttributeDefinitons.java +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SharedAttributeDefinitons.java @@ -200,6 +200,13 @@ public class SharedAttributeDefinitons { .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) .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 ATTRIBUTES = new ArrayList(); static { @@ -230,6 +237,7 @@ public class SharedAttributeDefinitons { ATTRIBUTES.add(AUTODETECT_BEARER_ONLY); ATTRIBUTES.add(IGNORE_OAUTH_QUERY_PARAMETER); ATTRIBUTES.add(PROXY_URL); + ATTRIBUTES.add(VERIFY_TOKEN_AUDIENCE); } private static boolean isSet(ModelNode attributes, SimpleAttributeDefinition def) { diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties index 769800cfce..8678ae5c11 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties @@ -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.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.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.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.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.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.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.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.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.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.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-server.credential=Credential value diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd index 18080d6be1..62ce35d28d 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd @@ -71,6 +71,7 @@ + @@ -116,9 +117,11 @@ + + diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml b/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml index 0b703b8c6b..367aec1757 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml @@ -53,6 +53,7 @@ false 10 20 + 3600 MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB @@ -60,6 +61,7 @@ EXTERNAL 443 http://localhost:9000 + true 0aa31d98-e0aa-404c-b6e0-e771dba1e798 api/$1/ diff --git a/core/src/main/java/org/keycloak/TokenVerifier.java b/core/src/main/java/org/keycloak/TokenVerifier.java index 39eeed0387..71e125f159 100755 --- a/core/src/main/java/org/keycloak/TokenVerifier.java +++ b/core/src/main/java/org/keycloak/TokenVerifier.java @@ -133,6 +133,37 @@ public class TokenVerifier { } }; + + public static class AudienceCheck implements Predicate { + + 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 Class clazz; private PublicKey publicKey; @@ -311,6 +342,16 @@ public class TokenVerifier { 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 audience(String expectedAudience) { + return this.replaceCheck(AudienceCheck.class, true, new AudienceCheck(expectedAudience)); + } + public TokenVerifier parse() throws VerificationException { if (jws == null) { if (tokenString == null) { diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java index 2eb5089921..095a61362f 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java @@ -40,7 +40,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; "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", "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 { @@ -85,6 +85,8 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien protected boolean pkce = false; @JsonProperty("ignore-oauth-query-parameter") 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}. @@ -268,4 +270,12 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien public void setIgnoreOAuthQueryParameter(boolean ignoreOAuthQueryParameter) { this.ignoreOAuthQueryParameter = ignoreOAuthQueryParameter; } + + public boolean isVerifyTokenAudience() { + return verifyTokenAudience; + } + + public void setVerifyTokenAudience(boolean verifyTokenAudience) { + this.verifyTokenAudience = verifyTokenAudience; + } } diff --git a/core/src/test/java/org/keycloak/RSAVerifierTest.java b/core/src/test/java/org/keycloak/RSAVerifierTest.java index 8418f14572..26f706034b 100755 --- a/core/src/test/java/org/keycloak/RSAVerifierTest.java +++ b/core/src/test/java/org/keycloak/RSAVerifierTest.java @@ -248,9 +248,45 @@ public class RSAVerifierTest { AccessToken v = null; try { v = verifySkeletonKeyToken(encoded); + Assert.fail(); } 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(); + } + } diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java index a53bc8c3bd..5721ecb99d 100644 --- a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java +++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java @@ -31,7 +31,7 @@ import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.ServerRequest; 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.util.StreamUtil; import org.keycloak.common.util.UriUtils; @@ -43,7 +43,7 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.ByteArrayOutputStream; + import java.io.IOException; import java.io.InputStream; 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 { String token = tokenResponse.getToken(); 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(REFRESH_TOKEN, refreshToken); req.getSession().setAttribute(TOKEN_PARSED, tokenParsed); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java index 8dadaafc18..325907d75e 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java @@ -22,13 +22,17 @@ import org.keycloak.authentication.ClientAuthenticator; import org.keycloak.authentication.ClientAuthenticatorFactory; import org.keycloak.authorization.admin.AuthorizationService; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientScopeModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.ClientInstallationProvider; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; import org.keycloak.services.managers.ClientManager; import org.keycloak.util.JsonSerialization; @@ -64,6 +68,10 @@ public class KeycloakOIDCClientInstallation implements ClientInstallationProvide rep.setCredentials(adapterConfig); } + if (showVerifyTokenAudience(client)) { + rep.setVerifyTokenAudience(true); + } + configureAuthorizationSettings(session, client, rep); 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 public String getProtocol() { return OIDCLoginProtocol.LOGIN_PROTOCOL; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCJbossSubsystemClientInstallation.java b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCJbossSubsystemClientInstallation.java index d0bc939eb2..dd762dcc45 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCJbossSubsystemClientInstallation.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCJbossSubsystemClientInstallation.java @@ -49,6 +49,11 @@ public class KeycloakOIDCJbossSubsystemClientInstallation implements ClientInsta } buffer.append(" ").append(realm.getSslRequired().name()).append("\n"); buffer.append(" ").append(client.getClientId()).append("\n"); + + if (KeycloakOIDCClientInstallation.showVerifyTokenAudience(client)) { + buffer.append(" true\n"); + } + String cred = client.getSecret(); if (KeycloakOIDCClientInstallation.showClientCredentialsAdapterConfig(client)) { Map adapterConfig = KeycloakOIDCClientInstallation.getClientCredentialsAdapterConfig(session, client); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java index 8cd4eae62f..bf63858d46 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java @@ -36,7 +36,7 @@ public class AudienceProtocolMapper extends AbstractOIDCProtocolMapper implement private static final List configProperties = new ArrayList(); - 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_HELP_TEXT = "included.client.audience.tooltip"; diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java index d391e0468f..b78aaeec5c 100644 --- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java @@ -210,7 +210,7 @@ public class ClientManager { } @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"}) public static class InstallationAdapterConfig extends BaseRealmConfig { @JsonProperty("resource") @@ -223,6 +223,8 @@ public class ClientManager { protected Boolean publicClient; @JsonProperty("credentials") protected Map credentials; + @JsonProperty("verify-token-audience") + protected Boolean verifyTokenAudience; @JsonProperty("policy-enforcer") protected PolicyEnforcerConfig enforcerConfig; @@ -250,6 +252,14 @@ public class ClientManager { this.credentials = credentials; } + public Boolean getVerifyTokenAudience() { + return verifyTokenAudience; + } + + public void setVerifyTokenAudience(Boolean verifyTokenAudience) { + this.verifyTokenAudience = verifyTokenAudience; + } + public Boolean getPublicClient() { return publicClient; } diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/CustomerServlet.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/CustomerServlet.java index f92845436a..e58a905709 100644 --- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/CustomerServlet.java +++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/CustomerServlet.java @@ -74,21 +74,20 @@ public class CustomerServlet extends HttpServlet { //try { - StringBuilder result = new StringBuilder(); String urlBase = ServletTestUtils.getUrlBase(req); - URL url = new URL(urlBase + "/customer-db/"); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("GET"); - conn.setRequestProperty(HttpHeaders.AUTHORIZATION, "Bearer " + context.getTokenString()); - BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); - String line; - while ((line = rd.readLine()) != null) { - result.append(line); + // Decide what to call based on the URL suffix + String serviceUrl; + if (req.getRequestURI().endsWith("/call-customer-db-audience-required")) { + serviceUrl = urlBase + "/customer-db-audience-required/"; + } else { + serviceUrl = urlBase + "/customer-db/"; } - rd.close(); + + String result = invokeService(serviceUrl, context); + resp.setContentType("text/html"); - pw.println(result.toString()); + pw.println(result); pw.flush(); // // 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(); + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/CustomerDbAudienceRequired.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/CustomerDbAudienceRequired.java new file mode 100644 index 0000000000..1ba1171242 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/CustomerDbAudienceRequired.java @@ -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 Marek Posolda + */ +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; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/CustomerPortal.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/CustomerPortal.java index 51c0978ba6..70395f98a1 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/CustomerPortal.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/CustomerPortal.java @@ -44,4 +44,14 @@ public class CustomerPortal extends AbstractPageWithInjectedUrl { 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; + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/jaas/LoginModulesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/jaas/LoginModulesTest.java new file mode 100644 index 0000000000..03000ed364 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/jaas/LoginModulesTest.java @@ -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 Marek Posolda + */ +public class LoginModulesTest extends AbstractKeycloakTest { + + @Override + public void addTestRealms(List 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 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 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 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 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 }; + } + }; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java index f2fad1c40a..5149f5831d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java @@ -76,6 +76,7 @@ import org.keycloak.testsuite.adapter.page.BasicAuth; import org.keycloak.testsuite.adapter.page.ClientSecretJwtSecurePortal; import org.keycloak.testsuite.adapter.page.CustomerCookiePortal; 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.CustomerPortal; 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); } + @Deployment(name = CustomerDbAudienceRequired.DEPLOYMENT_NAME) + protected static WebArchive customerDbAudienceRequired() { + return servletDeployment(CustomerDbAudienceRequired.DEPLOYMENT_NAME, AdapterActionsFilter.class, CustomerDatabaseServlet.class); + } + @Deployment(name = CustomerDbErrorPage.DEPLOYMENT_NAME) protected static WebArchive customerDbErrorPage() { 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 public void testBasicAuth() { String value = "hello"; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java index 6ff3bbe921..d759263871 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java @@ -17,14 +17,20 @@ package org.keycloak.testsuite.admin.client; +import javax.ws.rs.core.Response; + import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; 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.admin.ApiUtil; 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.hamcrest.Matchers.*; @@ -97,6 +103,27 @@ public class InstallationTest extends AbstractClientTest { assertThat(json, containsString("bearer-only")); assertThat(json, not(containsString("public-client"))); 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 diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AdminEventPaths.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AdminEventPaths.java index 980465da52..0992b5e277 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AdminEventPaths.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AdminEventPaths.java @@ -170,6 +170,11 @@ public class AdminEventPaths { 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) { URI uri = UriBuilder.fromUri(clientScopeResourcePath(clientScopeDbId)).path(ClientScopeResource.class, "getScopeMappings") .path(RoleMappingResource.class, "realmLevel") diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db-audience-required/META-INF/context.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db-audience-required/META-INF/context.xml new file mode 100644 index 0000000000..b4ddcce386 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db-audience-required/META-INF/context.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db-audience-required/WEB-INF/jetty-web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db-audience-required/WEB-INF/jetty-web.xml new file mode 100644 index 0000000000..8c59313878 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db-audience-required/WEB-INF/jetty-web.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db-audience-required/WEB-INF/keycloak.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db-audience-required/WEB-INF/keycloak.json new file mode 100644 index 0000000000..34c531021c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db-audience-required/WEB-INF/keycloak.json @@ -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 + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db-audience-required/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db-audience-required/WEB-INF/web.xml new file mode 100644 index 0000000000..56ed0e725a --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db-audience-required/WEB-INF/web.xml @@ -0,0 +1,75 @@ + + + + + + customer-db + + + + AdapterActionsFilter + org.keycloak.testsuite.adapter.filter.AdapterActionsFilter + + + + Servlet + org.keycloak.testsuite.adapter.servlet.CustomerDatabaseServlet + + + + AdapterActionsFilter + /* + + + + Servlet + /* + + + + + Users + /* + + + user + + + + + Unsecured + /unsecured/* + + + + + KEYCLOAK + demo + + + + admin + + + user + + + diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-portal/WEB-INF/keycloak.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-portal/WEB-INF/keycloak.json index 32703f9a12..e6e9c427db 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-portal/WEB-INF/keycloak.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-portal/WEB-INF/keycloak.json @@ -1,7 +1,7 @@ { "realm": "demo", "resource": "customer-portal", - "auth-server-url": "http://localhostt:8180/auth", + "auth-server-url": "http://localhost:8180/auth", "ssl-required" : "external", "expose-token": true, "min-time-between-jwks-requests": 120, diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json index bd1ced29c2..a45098c6c8 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json @@ -133,6 +133,13 @@ "baseUrl": "/customer-db", "bearerOnly": true }, + { + "clientId": "customer-db-audience-required", + "enabled": true, + "adminUrl": "/customer-db-audience-required", + "baseUrl": "/customer-db-audience-required", + "bearerOnly": true + }, { "clientId": "customer-portal", "enabled": true,