From cd2557e55df3ff587f57429bb98e867dc9ee3f80 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Thu, 23 Jun 2022 17:18:04 -0400 Subject: [PATCH 01/12] [ELY-2362] Fix invalid configuration logic for bearer auth --- .../security/http/oidc/OidcClientConfigurationBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java index 536f428fe61..9f0a319d7b6 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java @@ -139,7 +139,7 @@ protected OidcClientConfiguration internalBuild(final OidcJsonConfiguration oidc oidcClientConfiguration.setVerifyTokenAudience(oidcJsonConfiguration.isVerifyTokenAudience()); if (realmKeyPem == null && oidcJsonConfiguration.isBearerOnly() - && (oidcJsonConfiguration.getAuthServerUrl() == null || oidcJsonConfiguration.getProviderUrl() == null)) { + && (oidcJsonConfiguration.getAuthServerUrl() == null && oidcJsonConfiguration.getProviderUrl() == null)) { throw log.invalidConfigurationForBearerAuth(); } if ((oidcJsonConfiguration.getAuthServerUrl() == null && oidcJsonConfiguration.getProviderUrl() == null) && (!oidcClientConfiguration.isBearerOnly() || realmKeyPem == null)) { From 0c62f940c5d264ef200029821524c4209c05e553 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Tue, 28 Jun 2022 18:13:28 -0400 Subject: [PATCH 02/12] [ELY-2362] Add support for the bearer-only option when using the OIDC HTTP mechanism --- .../wildfly/security/http/HttpConstants.java | 21 ++ .../BearerTokenAuthenticationMechanism.java | 4 +- .../security/http/oidc/AccessToken.java | 10 + .../oidc/BearerTokenRequestAuthenticator.java | 205 ++++++++++++++++++ .../security/http/oidc/ElytronMessages.java | 17 ++ .../wildfly/security/http/oidc/IDToken.java | 97 --------- .../security/http/oidc/JsonWebToken.java | 104 ++++++++- .../org/wildfly/security/http/oidc/Oidc.java | 5 +- .../security/http/oidc/OidcHttpFacade.java | 10 +- .../http/oidc/RequestAuthenticator.java | 80 ++++++- .../security/http/oidc/TokenValidator.java | 100 +++++++-- 11 files changed, 522 insertions(+), 131 deletions(-) create mode 100644 http/oidc/src/main/java/org/wildfly/security/http/oidc/BearerTokenRequestAuthenticator.java diff --git a/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java b/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java index badae19cc8d..24eafd2acc4 100644 --- a/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java +++ b/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java @@ -17,6 +17,8 @@ */ package org.wildfly.security.http; +import java.util.regex.Pattern; + import org.ietf.jgss.GSSManager; /** @@ -120,6 +122,7 @@ private HttpConstants() { public static final String NEGOTIATE = "Negotiate"; public static final String NEXT_NONCE = "nextnonce"; public static final String NONCE = "nonce"; + public static final String PARTIAL = "partial/"; public static final String OPAQUE = "opaque"; public static final String QOP = "qop"; public static final String REALM = "realm"; @@ -129,16 +132,29 @@ private HttpConstants() { public static final String URI = "uri"; public static final String USERNAME = "username"; public static final String USERNAME_STAR = "username*"; + public static final String XML_HTTP_REQUEST = "XMLHttpRequest"; /* * Header Names */ + public static final String ACCEPT = "Accept"; public static final String AUTHENTICATION_INFO = "Authentication-Info"; public static final String AUTHORIZATION = "Authorization"; + public static final String FACES_REQUEST = "Faces-Request"; public static final String HOST = "Host"; public static final String LOCATION = "Location"; + public static final String SOAP_ACTION = "SOAPAction"; public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; + public static final String X_REQUESTED_WITH = "X-Requested-With"; + + /** + * Errors + */ + public static final String ERROR = "error"; + public static final String ERROR_DESCRIPTION = "error_description"; + public static final String INVALID_TOKEN = "invalid_token"; + public static final String STALE_TOKEN = "Stale token"; /* * Mechanism Names @@ -187,4 +203,9 @@ private HttpConstants() { public static final String HTTP = "http"; public static final String HTTPS = "https"; + /** + * Bearer token pattern. + */ + public static final Pattern BEARER_TOKEN_PATTERN = Pattern.compile("^Bearer *([^ ]+) *$", Pattern.CASE_INSENSITIVE); + } diff --git a/http/bearer/src/main/java/org/wildfly/security/http/bearer/BearerTokenAuthenticationMechanism.java b/http/bearer/src/main/java/org/wildfly/security/http/bearer/BearerTokenAuthenticationMechanism.java index ebabe9553fd..85d0d417883 100644 --- a/http/bearer/src/main/java/org/wildfly/security/http/bearer/BearerTokenAuthenticationMechanism.java +++ b/http/bearer/src/main/java/org/wildfly/security/http/bearer/BearerTokenAuthenticationMechanism.java @@ -19,6 +19,7 @@ package org.wildfly.security.http.bearer; import static org.wildfly.security.http.HttpConstants.BEARER_TOKEN; +import static org.wildfly.security.http.HttpConstants.BEARER_TOKEN_PATTERN; import static org.wildfly.security.http.HttpConstants.FORBIDDEN; import static org.wildfly.security.http.HttpConstants.REALM; import static org.wildfly.security.http.HttpConstants.UNAUTHORIZED; @@ -28,7 +29,6 @@ import java.io.IOException; import java.util.List; import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; @@ -65,8 +65,6 @@ */ final class BearerTokenAuthenticationMechanism implements HttpServerAuthenticationMechanism { - private static final Pattern BEARER_TOKEN_PATTERN = Pattern.compile("^Bearer *([^ ]+) *$", Pattern.CASE_INSENSITIVE); - private final CallbackHandler callbackHandler; BearerTokenAuthenticationMechanism(CallbackHandler callbackHandler) { diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/AccessToken.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/AccessToken.java index b6cc11cd6c9..c89f60b67ad 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/AccessToken.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/AccessToken.java @@ -34,6 +34,7 @@ public class AccessToken extends JsonWebToken { private static final String ALLOWED_ORIGINS = "allowed-origins"; private static final String REALM_ACCESS = "realm_access"; private static final String RESOURCE_ACCESS = "resource_access"; + private static final String TRUSTED_CERTS = "trusted-certs"; /** * Construct a new instance. @@ -95,4 +96,13 @@ public RealmAccessClaim getResourceAccessClaim(String resource) { Map realmAccessClaimMap = getResourceAccessClaim(); return realmAccessClaimMap == null ? null : realmAccessClaimMap.get(resource); } + + /** + * Get the trusted-certs claim. + * + * @return the trusted-certs claim + */ + public List getTrustedCertsClaim() { + return getStringListClaimValue(TRUSTED_CERTS); + } } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/BearerTokenRequestAuthenticator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/BearerTokenRequestAuthenticator.java new file mode 100644 index 00000000000..b14f363efa3 --- /dev/null +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/BearerTokenRequestAuthenticator.java @@ -0,0 +1,205 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2022 Red Hat, Inc., and individual 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.wildfly.security.http.oidc; + +import static org.wildfly.security.http.HttpConstants.BEARER_TOKEN_PATTERN; +import static org.wildfly.security.http.HttpConstants.ERROR; +import static org.wildfly.security.http.HttpConstants.ERROR_DESCRIPTION; +import static org.wildfly.security.http.HttpConstants.INVALID_TOKEN; +import static org.wildfly.security.http.HttpConstants.REALM; +import static org.wildfly.security.http.HttpConstants.STALE_TOKEN; +import static org.wildfly.security.http.HttpConstants.WWW_AUTHENTICATE; +import static org.wildfly.security.http.oidc.ElytronMessages.log; +import static org.wildfly.security.http.oidc.Oidc.logToken; + +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.regex.Matcher; + +import org.apache.http.HttpStatus; +import org.wildfly.security.http.HttpConstants; + +/** + * @author Bill Burke + * @author Farah Juma + */ +public class BearerTokenRequestAuthenticator { + protected OidcHttpFacade facade; + protected OidcClientConfiguration oidcClientConfiguration; + protected AuthChallenge challenge; + protected String tokenString; + private AccessToken token; + private String surrogate; + + public BearerTokenRequestAuthenticator(OidcHttpFacade facade, OidcClientConfiguration oidcClientConfiguration) { + this.facade = facade; + this.oidcClientConfiguration = oidcClientConfiguration; + } + + public AuthChallenge getChallenge() { + return challenge; + } + + public String getTokenString() { + return tokenString; + } + + public AccessToken getToken() { + return token; + } + + public String getSurrogate() { + return surrogate; + } + + public Oidc.AuthOutcome authenticate() { + List authorizationValues = facade.getRequest().getHeaders(HttpConstants.AUTHORIZATION); + if (authorizationValues == null || authorizationValues.isEmpty()) { + challenge = challengeResponse(AuthenticationError.Reason.NO_BEARER_TOKEN, null, null); + return Oidc.AuthOutcome.NOT_ATTEMPTED; + } + + Matcher matcher; + for (String authorizationValue : authorizationValues) { + if ((matcher = BEARER_TOKEN_PATTERN.matcher(authorizationValue)).matches()) { + tokenString = matcher.group(1); + log.debugf("Found [%d] values in authorization header, selecting the first value for Bearer", (Integer) authorizationValues.size()); + break; + } + } + if (tokenString == null) { + challenge = challengeResponse(AuthenticationError.Reason.NO_BEARER_TOKEN, null, null); + return Oidc.AuthOutcome.NOT_ATTEMPTED; + } + return verifyToken(tokenString); + } + + protected Oidc.AuthOutcome verifyToken(final String tokenString) { + log.debug("Verifying access_token"); + logToken("\taccess_token", tokenString); + try { + TokenValidator tokenValidator = TokenValidator.builder(oidcClientConfiguration).build(); + token = tokenValidator.parseAndVerifyToken(tokenString); + log.debug("Token Verification succeeded!"); + } catch (OidcException e) { + log.failedVerificationOfToken(e.getMessage()); + challenge = challengeResponse(AuthenticationError.Reason.INVALID_TOKEN, INVALID_TOKEN, e.getMessage()); + return Oidc.AuthOutcome.FAILED; + } + + if (token.getIssuedAt() < oidcClientConfiguration.getNotBefore()) { + log.debug("Stale token"); + challenge = challengeResponse(AuthenticationError.Reason.STALE_TOKEN, INVALID_TOKEN, STALE_TOKEN); + return Oidc.AuthOutcome.FAILED; + } + + // these are Keycloak-specific checks + boolean verifyCaller; + if (oidcClientConfiguration.isUseResourceRoleMappings()) { + verifyCaller = isVerifyCaller(token.getResourceAccessClaim(oidcClientConfiguration.getResourceName())); + } else { + verifyCaller = isVerifyCaller(token.getRealmAccessClaim()); + } + if (verifyCaller) { + List trustedCerts = token.getTrustedCertsClaim(); + if (trustedCerts == null || trustedCerts.isEmpty()) { + log.noTrustedCertificatesInToken(); + challenge = clientCertChallenge(); + return Oidc.AuthOutcome.FAILED; + } + + // simply make sure mutual TLS auth took place + Certificate[] chain = facade.getCertificateChain(); + if (chain == null || chain.length == 0) { + log.noPeerCertificatesEstablishedOnConnection(); + challenge = clientCertChallenge(); + return Oidc.AuthOutcome.FAILED; + } + surrogate = ((X509Certificate) chain[0]).getSubjectDN().getName(); + } + + log.debug("Successfully authorized"); + return Oidc.AuthOutcome.AUTHENTICATED; + } + + private boolean isVerifyCaller(RealmAccessClaim accessClaim) { + if (accessClaim != null && accessClaim.getVerifyCaller() != null) { + return accessClaim.getVerifyCaller().booleanValue(); + } + return false; + } + + protected AuthChallenge challengeResponse(final AuthenticationError.Reason reason, final String error, final String description) { + StringBuilder header = new StringBuilder("Bearer"); + if (oidcClientConfiguration.getRealm() != null) { + header.append(" ").append(REALM).append("=\"").append(oidcClientConfiguration.getRealm()).append("\""); + if (error != null || description != null) { + header.append(","); + } + } + if (error != null) { + header.append(" ").append(ERROR).append("=\"").append(error).append("\""); + if (description != null) { + header.append(","); + } + } + if (description != null) { + header.append(" ").append(ERROR_DESCRIPTION).append("=\"").append(description).append("\""); + } + + final String challenge = header.toString(); + return new AuthChallenge() { + @Override + public int getResponseCode() { + return HttpStatus.SC_UNAUTHORIZED; + } + + @Override + public boolean challenge(OidcHttpFacade facade) { + AuthenticationError error = new AuthenticationError(reason, description); + facade.getRequest().setError(error); + facade.getResponse().addHeader(WWW_AUTHENTICATE, challenge); + if(oidcClientConfiguration.isDelegateBearerErrorResponseSending()){ + facade.getResponse().setStatus(HttpStatus.SC_UNAUTHORIZED); + } + else { + facade.getResponse().sendError(HttpStatus.SC_UNAUTHORIZED); + } + return true; + } + }; + } + + protected AuthChallenge clientCertChallenge() { + return new AuthChallenge() { + @Override + public int getResponseCode() { + return 0; + } + + @Override + public boolean challenge(OidcHttpFacade facade) { + // do the same thing as client cert auth + return false; + } + }; + } + +} diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/ElytronMessages.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/ElytronMessages.java index fb61affc433..89ce19fa64f 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/ElytronMessages.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/ElytronMessages.java @@ -208,5 +208,22 @@ interface ElytronMessages extends BasicLogger { @Message(id = 23049, value = "Invalid 'auth-server-url' or 'provider-url': '%s'") void invalidAuthServerUrlOrProviderUrl(String url); + @Message(id = 23050, value = "Invalid bearer token claims") + OidcException invalidBearerTokenClaims(); + + @Message(id = 23051, value = "Invalid bearer token") + OidcException invalidBearerToken(@Cause Throwable cause); + + @LogMessage(level = WARN) + @Message(id = 23052, value = "No trusted certificates in token") + void noTrustedCertificatesInToken(); + + @LogMessage(level = WARN) + @Message(id = 23053, value = "No peer certificates established on the connection") + void noPeerCertificatesEstablishedOnConnection(); + + @Message(id = 23054, value = "Unexpected value for typ claim") + String unexpectedValueForTypeClaim(); + } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/IDToken.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/IDToken.java index ee5c64e14e2..80dd22c6ec4 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/IDToken.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/IDToken.java @@ -36,16 +36,9 @@ public class IDToken extends JsonWebToken { public static final String AT_HASH = "at_hash"; public static final String C_HASH = "c_hash"; - public static final String NAME = "name"; - public static final String GIVEN_NAME = "given_name"; - public static final String FAMILY_NAME = "family_name"; - public static final String MIDDLE_NAME = "middle_name"; - public static final String NICKNAME = "nickname"; - public static final String PREFERRED_USERNAME = "preferred_username"; public static final String PROFILE = "profile"; public static final String PICTURE = "picture"; public static final String WEBSITE = "website"; - public static final String EMAIL = "email"; public static final String EMAIL_VERIFIED = "email_verified"; public static final String GENDER = "gender"; public static final String BIRTHDATE = "birthdate"; @@ -58,7 +51,6 @@ public class IDToken extends JsonWebToken { public static final String CLAIMS_LOCALES = "claims_locales"; public static final String ACR = "acr"; public static final String S_HASH = "s_hash"; - public static final String SUB = "sub"; /** * Construct a new instance. @@ -69,60 +61,6 @@ public IDToken(JwtClaims jwtClaims) { super(jwtClaims); } - /** - * Get the name claim. - * - * @return the name claim - */ - public String getName() { - return getClaimValueAsString(NAME); - } - - /** - * Get the given name claim. - * - * @return the given name claim - */ - public String getGivenName() { - return getClaimValueAsString(GIVEN_NAME); - } - - /** - * Get the family name claim. - * - * @return the family name claim - */ - public String getFamilyName() { - return getClaimValueAsString(FAMILY_NAME); - } - - /** - * Get the middle name claim. - * - * @return the middle name claim - */ - public String getMiddleName() { - return getClaimValueAsString(MIDDLE_NAME); - } - - /** - * Get the nick name claim. - * - * @return the nick name claim - */ - public String getNickName() { - return getClaimValueAsString(NICKNAME); - } - - /** - * Get the preferred username claim. - * - * @return the preferred username claim - */ - public String getPreferredUsername() { - return getClaimValueAsString(PREFERRED_USERNAME); - } - /** * Get the profile claim. * @@ -150,15 +88,6 @@ public String getWebsite() { return getClaimValueAsString(WEBSITE); } - /** - * Get the email claim. - * - * @return the email claim - */ - public String getEmail() { - return getClaimValueAsString(EMAIL); - } - /** * Get the email verified claim. * @@ -291,30 +220,4 @@ public String getAcr() { return getClaimValueAsString(ACR); } - public String getPrincipalName(OidcClientConfiguration deployment) { - String attr = SUB; - if (deployment.getPrincipalAttribute() != null) { - attr = deployment.getPrincipalAttribute(); - } - switch (attr) { - case SUB: - return getSubject(); - case EMAIL: - return getEmail(); - case PREFERRED_USERNAME: - return getPreferredUsername(); - case NAME: - return getName(); - case GIVEN_NAME: - return getGivenName(); - case FAMILY_NAME: - return getFamilyName(); - case NICKNAME: - return getNickName(); - default: - return getSubject(); - } - } - - } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/JsonWebToken.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/JsonWebToken.java index 27553cf6438..6be80b75496 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/JsonWebToken.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/JsonWebToken.java @@ -44,10 +44,17 @@ */ public class JsonWebToken { + public static final String EMAIL = "email"; public static final String EXP = "exp"; - public static final String NBF = "nbf"; + public static final String FAMILY_NAME = "family_name"; + public static final String GIVEN_NAME = "given_name"; public static final String IAT = "iat"; - + public static final String MIDDLE_NAME = "middle_name"; + public static final String NAME = "name"; + public static final String NICKNAME = "nickname"; + public static final String NBF = "nbf"; + public static final String PREFERRED_USERNAME = "preferred_username"; + public static final String SUB = "sub"; private final JwtClaims jwtClaims; @@ -255,6 +262,99 @@ public List getStringListClaimValue(String claimName) { } } + /** + * Get the name claim. + * + * @return the name claim + */ + public String getName() { + return getClaimValueAsString(NAME); + } + + /** + * Get the principal name. + * @param deployment the OIDC client configuration that should be used to determine the principal + * @return the principal name + */ + public String getPrincipalName(OidcClientConfiguration deployment) { + String attr = SUB; + if (deployment.getPrincipalAttribute() != null) { + attr = deployment.getPrincipalAttribute(); + } + switch (attr) { + case SUB: + return getSubject(); + case EMAIL: + return getEmail(); + case PREFERRED_USERNAME: + return getPreferredUsername(); + case NAME: + return getName(); + case GIVEN_NAME: + return getGivenName(); + case FAMILY_NAME: + return getFamilyName(); + case NICKNAME: + return getNickName(); + default: + return getSubject(); + } + } + + /** + * Get the given name claim. + * + * @return the given name claim + */ + public String getGivenName() { + return getClaimValueAsString(GIVEN_NAME); + } + + /** + * Get the family name claim. + * + * @return the family name claim + */ + public String getFamilyName() { + return getClaimValueAsString(FAMILY_NAME); + } + + /** + * Get the middle name claim. + * + * @return the middle name claim + */ + public String getMiddleName() { + return getClaimValueAsString(MIDDLE_NAME); + } + + /** + * Get the nick name claim. + * + * @return the nick name claim + */ + public String getNickName() { + return getClaimValueAsString(NICKNAME); + } + + /** + * Get the preferred username claim. + * + * @return the preferred username claim + */ + public String getPreferredUsername() { + return getClaimValueAsString(PREFERRED_USERNAME); + } + + /** + * Get the email claim. + * + * @return the email claim + */ + public String getEmail() { + return getClaimValueAsString(EMAIL); + } + private static int getCurrentTimeInSeconds() { return ((int) (System.currentTimeMillis() / 1000)); } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java index e934777c637..39e01421582 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java @@ -46,7 +46,9 @@ public class Oidc { public static final String OIDC_NAME = "OIDC"; public static final String JSON_CONTENT_TYPE = "application/json"; - public static final String HTML_CONTEXT_TYPE = "text/html"; + public static final String HTML_CONTENT_TYPE = "text/html"; + public static final String WILDCARD_CONTENT_TYPE = "*/*"; + public static final String TEXT_CONTENT_TYPE = "text/*"; public static final String DISCOVERY_PATH = ".well-known/openid-configuration"; public static final String KEYCLOAK_REALMS_PATH = "realms/"; public static final String JSON_CONFIG_CONTEXT_PARAM = "org.wildfly.security.http.oidc.json.config"; @@ -72,6 +74,7 @@ public class Oidc { public static final String STATE = "state"; public static final int INVALID_ISSUED_FOR_CLAIM = -1; public static final int INVALID_AT_HASH_CLAIM = -2; + public static final int INVALID_TYPE_CLAIM = -3; static final String OIDC_CLIENT_CONFIG_RESOLVER = "oidc.config.resolver"; static final String OIDC_CONFIG_FILE_LOCATION = "oidc.config.file"; static final String OIDC_JSON_FILE = "/WEB-INF/oidc.json"; diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcHttpFacade.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcHttpFacade.java index 6fe0e557348..f300cb7a955 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcHttpFacade.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcHttpFacade.java @@ -19,7 +19,7 @@ package org.wildfly.security.http.oidc; import static org.wildfly.security.http.oidc.ElytronMessages.log; -import static org.wildfly.security.http.oidc.Oidc.HTML_CONTEXT_TYPE; +import static org.wildfly.security.http.oidc.Oidc.HTML_CONTENT_TYPE; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; @@ -33,6 +33,7 @@ import java.net.URLDecoder; import java.security.Principal; +import java.security.cert.Certificate; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -43,7 +44,6 @@ import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.cert.X509Certificate; import javax.security.sasl.AuthorizeCallback; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; @@ -455,7 +455,7 @@ public void sendError(int code) { public void sendError(final int code, final String message) { responseConsumer = responseConsumer.andThen(response -> { response.setStatusCode(code); - response.addResponseHeader("Content-Type", HTML_CONTEXT_TYPE); + response.addResponseHeader("Content-Type", HTML_CONTENT_TYPE); try { response.getOutputStream().write(message.getBytes()); } catch (IOException e) { @@ -471,8 +471,8 @@ public void end() { }; } - public X509Certificate[] getCertificateChain() { - return new X509Certificate[0]; + public Certificate[] getCertificateChain() { + return request.getPeerCertificates(); } public OidcSecurityContext getSecurityContext() { diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java index 864517d55b9..360fac9637f 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java @@ -18,8 +18,20 @@ package org.wildfly.security.http.oidc; +import static org.wildfly.security.http.HttpConstants.ACCEPT; +import static org.wildfly.security.http.HttpConstants.FACES_REQUEST; +import static org.wildfly.security.http.HttpConstants.PARTIAL; +import static org.wildfly.security.http.HttpConstants.SOAP_ACTION; +import static org.wildfly.security.http.HttpConstants.XML_HTTP_REQUEST; +import static org.wildfly.security.http.HttpConstants.X_REQUESTED_WITH; import static org.wildfly.security.http.oidc.ElytronMessages.log; import static org.wildfly.security.http.oidc.Oidc.AuthOutcome; +import static org.wildfly.security.http.oidc.Oidc.HTML_CONTENT_TYPE; +import static org.wildfly.security.http.oidc.Oidc.TEXT_CONTENT_TYPE; +import static org.wildfly.security.http.oidc.Oidc.WILDCARD_CONTENT_TYPE; + +import java.util.Collections; +import java.util.List; import org.wildfly.security.http.HttpScope; import org.wildfly.security.http.Scope; @@ -58,6 +70,10 @@ protected void completeOidcAuthentication(final OidcPrincipal principal) { + facade.authenticationComplete(new OidcAccount(principal), false); + } + protected String changeHttpSessionId(boolean create) { HttpScope session = facade.getScope(Scope.SESSION); if (create) { @@ -77,6 +93,34 @@ private AuthOutcome doAuthenticate() { log.trace("--> authenticate()"); } + if (log.isTraceEnabled()) { + log.trace("try bearer"); + } + + BearerTokenRequestAuthenticator bearer = new BearerTokenRequestAuthenticator(facade, deployment); + + AuthOutcome outcome = bearer.authenticate(); + if (outcome == AuthOutcome.FAILED) { + challenge = bearer.getChallenge(); + log.debug("Bearer FAILED"); + return AuthOutcome.FAILED; + } else if (outcome == AuthOutcome.AUTHENTICATED) { + if (verifySSL()) return AuthOutcome.FAILED; + completeAuthentication(bearer); + log.debug("Bearer AUTHENTICATED"); + return AuthOutcome.AUTHENTICATED; + } + if (deployment.isBearerOnly()) { + challenge = bearer.getChallenge(); + log.debug("NOT_ATTEMPTED: bearer only"); + return AuthOutcome.NOT_ATTEMPTED; + } + if (isAutodetectedBearerOnly(facade.getRequest())) { + challenge = bearer.getChallenge(); + log.debug("NOT_ATTEMPTED: Treating as bearer only"); + return AuthOutcome.NOT_ATTEMPTED; + } + if (log.isTraceEnabled()) { log.trace("try oidc"); } @@ -88,14 +132,13 @@ private AuthOutcome doAuthenticate() { } OidcRequestAuthenticator oidc = createOidcAuthenticator(); - AuthOutcome outcome = oidc.authenticate(); + outcome = oidc.authenticate(); if (outcome == AuthOutcome.FAILED) { challenge = oidc.getChallenge(); return AuthOutcome.FAILED; } else if (outcome == AuthOutcome.NOT_ATTEMPTED) { challenge = oidc.getChallenge(); return AuthOutcome.NOT_ATTEMPTED; - } if (verifySSL()) return AuthOutcome.FAILED; @@ -126,4 +169,37 @@ protected void completeAuthentication(OidcRequestAuthenticator oidc) { completeOidcAuthentication(principal); log.debugv("User ''{0}'' invoking ''{1}'' on client ''{2}''", principal.getName(), facade.getRequest().getURI(), deployment.getResourceName()); } + + protected void completeAuthentication(BearerTokenRequestAuthenticator bearer) { + RefreshableOidcSecurityContext session = new RefreshableOidcSecurityContext(deployment, null, bearer.getTokenString(), bearer.getToken(), null, null, null); + final OidcPrincipal principal = new OidcPrincipal<>(bearer.getToken().getPrincipalName(deployment), session); + completeBearerAuthentication(principal); + log.debugv("User ''{0}'' invoking ''{1}'' on client ''{2}''", principal.getName(), facade.getRequest().getURI(), deployment.getResourceName()); + } + + protected boolean isAutodetectedBearerOnly(OidcHttpFacade.Request request) { + if (! deployment.isAutodetectBearerOnly()) return false; + + String headerValue = facade.getRequest().getHeader(X_REQUESTED_WITH); + if (headerValue != null && headerValue.equalsIgnoreCase(XML_HTTP_REQUEST)) { + return true; + } + headerValue = facade.getRequest().getHeader(FACES_REQUEST); + if (headerValue != null && headerValue.startsWith(PARTIAL)) { + return true; + } + headerValue = facade.getRequest().getHeader(SOAP_ACTION); + if (headerValue != null) { + return true; + } + + List accepts = facade.getRequest().getHeaders(ACCEPT); + if (accepts == null) accepts = Collections.emptyList(); + for (String accept : accepts) { + if (accept.contains(HTML_CONTENT_TYPE) || accept.contains(TEXT_CONTENT_TYPE) || accept.contains(WILDCARD_CONTENT_TYPE)) { + return false; + } + } + return true; + } } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java index 899050f0cc9..a6f5bde57bc 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java @@ -22,6 +22,7 @@ import static org.wildfly.security.http.oidc.IDToken.AT_HASH; import static org.wildfly.security.http.oidc.Oidc.INVALID_AT_HASH_CLAIM; import static org.wildfly.security.http.oidc.Oidc.INVALID_ISSUED_FOR_CLAIM; +import static org.wildfly.security.http.oidc.Oidc.INVALID_TYPE_CLAIM; import static org.wildfly.security.http.oidc.Oidc.getJavaAlgorithmForHash; import static org.wildfly.security.jose.jwk.JWKUtil.BASE64_URL; @@ -44,7 +45,8 @@ import org.wildfly.common.iteration.ByteIterator; /** - * Validator for an ID token, as per OpenID Connect Core 1.0. + * Validator for an ID token or bearer token, as per OpenID Connect Core 1.0 + * and RFC 7523. * * @author Farah Juma */ @@ -63,26 +65,14 @@ private TokenValidator(Builder builder) { * Parse and verify the given ID token. * * @param idToken the ID token - * @return the {@code JwtContext} if the ID token was valid + * @return the {@code VerifiedTokens} if the ID token was valid * @throws OidcException if the ID token is invalid */ public VerifiedTokens parseAndVerifyToken(final String idToken, final String accessToken) throws OidcException { try { - // first pass to determine the kid, if present - JwtConsumer firstPassJwtConsumer = new JwtConsumerBuilder() - .setSkipAllValidators() - .setDisableRequireSignature() - .setSkipSignatureVerification() - .build(); - JwtContext idJwtContext = firstPassJwtConsumer.process(idToken); - String kid = idJwtContext.getJoseObjects().get(HEADER_INDEX).getKeyIdHeaderValue(); - if (kid != null && clientConfiguration.getPublicKeyLocator() != null) { - jwtConsumerBuilder.setVerificationKey(clientConfiguration.getPublicKeyLocator().getPublicKey(kid, clientConfiguration)); - } else { - // secret key - ClientSecretCredentialsProvider clientSecretCredentialsProvider = (ClientSecretCredentialsProvider) clientConfiguration.getClientAuthenticator(); - jwtConsumerBuilder.setVerificationKey(clientSecretCredentialsProvider.getClientSecret()); - } + JwtContext idJwtContext = setVerificationKey(idToken, jwtConsumerBuilder); + jwtConsumerBuilder.setExpectedAudience(clientConfiguration.getResourceName()); + jwtConsumerBuilder.registerValidator(new AzpValidator(clientConfiguration.getResourceName())); jwtConsumerBuilder.registerValidator(new AtHashValidator(accessToken, clientConfiguration.getTokenSignatureAlgorithm())); // second pass to validate jwtConsumerBuilder.build().processContext(idJwtContext); @@ -97,6 +87,55 @@ public VerifiedTokens parseAndVerifyToken(final String idToken, final String acc } } + /** + * Parse and verify the given bearer token. + * + * @param bearerToken the bearer token + * @return the {@code AccessToken} if the bearer token was valid + * @throws OidcException if the bearer token is invalid + */ + public AccessToken parseAndVerifyToken(final String bearerToken) throws OidcException { + try { + JwtContext jwtContext = setVerificationKey(bearerToken, jwtConsumerBuilder); + jwtConsumerBuilder.setRequireSubject(); + jwtConsumerBuilder.registerValidator(new TypeValidator("Bearer")); + if (clientConfiguration.isVerifyTokenAudience()) { + jwtConsumerBuilder.setExpectedAudience(clientConfiguration.getResourceName()); + } else { + jwtConsumerBuilder.setSkipDefaultAudienceValidation(); + } + // second pass to validate + jwtConsumerBuilder.build().processContext(jwtContext); + JwtClaims jwtClaims = jwtContext.getJwtClaims(); + if (jwtClaims == null) { + throw log.invalidBearerTokenClaims(); + } + return new AccessToken(jwtClaims); + } catch (InvalidJwtException e) { + log.tracef("Problem parsing bearer token: " + bearerToken, e); + throw log.invalidBearerToken(e); + } + } + + private JwtContext setVerificationKey(final String token, final JwtConsumerBuilder jwtConsumerBuilder) throws InvalidJwtException { + // first pass to determine the kid, if present + JwtConsumer firstPassJwtConsumer = new JwtConsumerBuilder() + .setSkipAllValidators() + .setDisableRequireSignature() + .setSkipSignatureVerification() + .build(); + JwtContext jwtContext = firstPassJwtConsumer.process(token); + String kid = jwtContext.getJoseObjects().get(HEADER_INDEX).getKeyIdHeaderValue(); + if (kid != null && clientConfiguration.getPublicKeyLocator() != null) { + jwtConsumerBuilder.setVerificationKey(clientConfiguration.getPublicKeyLocator().getPublicKey(kid, clientConfiguration)); + } else { + // secret key + ClientSecretCredentialsProvider clientSecretCredentialsProvider = (ClientSecretCredentialsProvider) clientConfiguration.getClientAuthenticator(); + jwtConsumerBuilder.setVerificationKey(clientSecretCredentialsProvider.getClientSecret()); + } + return jwtContext; + } + /** * Construct a new builder instance. * @@ -127,9 +166,9 @@ public static class Builder { } /** - * Create an ID token validator. + * Create an ID token or bearer token validator. * - * @return the newly created ID token validator + * @return the newly created token validator * @throws IllegalArgumentException if a required builder parameter is missing or invalid */ public TokenValidator build() throws IllegalArgumentException { @@ -156,10 +195,8 @@ public TokenValidator build() throws IllegalArgumentException { jwtConsumerBuilder = new JwtConsumerBuilder() .setExpectedIssuer(expectedIssuer) - .setExpectedAudience(clientId) .setJwsAlgorithmConstraints( new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT, expectedJwsAlgorithm)) - .registerValidator(new AzpValidator(clientId)) .setRequireExpirationTime(); return new TokenValidator(this); @@ -222,6 +259,27 @@ public ErrorCodeValidator.Error validate(JwtContext jwtContext) throws Malformed } } + private static class TypeValidator implements ErrorCodeValidator { + public static final String TYPE = "typ"; + private final String expectedType; + + public TypeValidator(String expectedType) { + this.expectedType = expectedType; + } + + public ErrorCodeValidator.Error validate(JwtContext jwtContext) throws MalformedClaimException { + JwtClaims jwtClaims = jwtContext.getJwtClaims(); + boolean valid = false; + if (jwtClaims.hasClaim(TYPE)) { + valid = jwtClaims.getStringClaimValue(TYPE).equals(expectedType); + } + if (! valid) { + return new ErrorCodeValidator.Error(INVALID_TYPE_CLAIM, log.unexpectedValueForTypeClaim()); + } + return null; + } + } + private static String getAccessTokenHash(String accessTokenString, String jwsAlgorithm) throws NoSuchAlgorithmException { byte[] inputBytes = accessTokenString.getBytes(StandardCharsets.UTF_8); String javaAlgName = getJavaAlgorithmForHash(jwsAlgorithm); From 01640ef0a9457048bc6a849043b30aad12c8242c Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Thu, 7 Jul 2022 17:48:38 -0400 Subject: [PATCH 03/12] [ELY-2362] Add the ability to handle CORS preflight requests --- .../wildfly/security/http/HttpConstants.java | 1 + .../oidc/OidcAuthenticationMechanism.java | 47 +++++++++++++++++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java b/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java index 24eafd2acc4..0449fb61dd7 100644 --- a/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java +++ b/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java @@ -187,6 +187,7 @@ private HttpConstants() { */ public static final String POST = "POST"; + public static final String OPTIONS = "OPTIONS"; /* * Algorithms diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcAuthenticationMechanism.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcAuthenticationMechanism.java index 57f184007fb..f1ea963375d 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcAuthenticationMechanism.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcAuthenticationMechanism.java @@ -18,6 +18,7 @@ package org.wildfly.security.http.oidc; +import static org.wildfly.security.http.HttpConstants.OPTIONS; import static org.wildfly.security.http.oidc.ElytronMessages.log; import static org.wildfly.security.http.oidc.Oidc.OIDC_CLIENT_CONTEXT_KEY; import static org.wildfly.security.http.oidc.Oidc.AuthOutcome; @@ -73,7 +74,8 @@ public void evaluateRequest(HttpServerRequest request) throws HttpAuthentication RequestAuthenticator authenticator = createRequestAuthenticator(httpFacade, oidcClientConfiguration); httpFacade.getTokenStore().checkCurrentToken(); - if (oidcClientConfiguration.getAuthServerBaseUrl() != null && keycloakPreActions(httpFacade, oidcClientContext)) { + if ((oidcClientConfiguration.getAuthServerBaseUrl() != null && keycloakPreActions(httpFacade, oidcClientConfiguration)) + || preflightCors(httpFacade, oidcClientConfiguration)) { log.debugf("Pre-actions has aborted the evaluation of [%s]", request.getRequestURI()); httpFacade.authenticationInProgress(); return; @@ -117,10 +119,49 @@ private int getConfidentialPort() { return 8443; } - private boolean keycloakPreActions(OidcHttpFacade httpFacade, OidcClientContext deploymentContext) { + private boolean keycloakPreActions(OidcHttpFacade httpFacade, OidcClientConfiguration oidcClientConfiguration) { NodesRegistrationManagement nodesRegistrationManagement = new NodesRegistrationManagement(); - nodesRegistrationManagement.tryRegister(httpFacade.getOidcClientConfiguration()); + nodesRegistrationManagement.tryRegister(oidcClientConfiguration); return false; } + private boolean preflightCors(OidcHttpFacade httpFacade, OidcClientConfiguration oidcClientConfiguration) { + String requestUri = httpFacade.getRequest().getURI(); + log.debugv("adminRequest {0}", requestUri); + if (! oidcClientConfiguration.isCors()) { + return false; + } + log.debugv("checkCorsPreflight {0}", httpFacade.getRequest().getURI()); + if (! httpFacade.getRequest().getMethod().equalsIgnoreCase(OPTIONS)) { + return false; + } + String origin = httpFacade.getRequest().getHeader(CorsHeaders.ORIGIN); + if (origin == null) { + log.debug("checkCorsPreflight: no origin header"); + return false; + } + log.debug("Preflight request returning"); + httpFacade.getResponse().setStatus(HttpStatus.SC_OK); + httpFacade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin); + httpFacade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + String requestMethods = httpFacade.getRequest().getHeader(CorsHeaders.ACCESS_CONTROL_REQUEST_METHOD); + if (requestMethods != null) { + if (oidcClientConfiguration.getCorsAllowedMethods() != null) { + requestMethods = oidcClientConfiguration.getCorsAllowedMethods(); + } + httpFacade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethods); + } + String allowHeaders = httpFacade.getRequest().getHeader(CorsHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + if (allowHeaders != null) { + if (oidcClientConfiguration.getCorsAllowedHeaders() != null) { + allowHeaders = oidcClientConfiguration.getCorsAllowedHeaders(); + } + httpFacade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_HEADERS, allowHeaders); + } + if (oidcClientConfiguration.getCorsMaxAge() > -1) { + httpFacade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_MAX_AGE, Integer.toString(oidcClientConfiguration.getCorsMaxAge())); + } + return true; + } + } From 201869acf3f44b36c83b6fd008369108ba86d339 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Fri, 8 Jul 2022 11:01:38 -0400 Subject: [PATCH 04/12] [ELY-2362] Ensure that a bearer token passed via an access_token query parameter can be processed appropriately --- ...eryParameterTokenRequestAuthenticator.java | 55 +++++++++++++++++++ .../http/oidc/RequestAuthenticator.java | 18 ++++++ 2 files changed, 73 insertions(+) create mode 100644 http/oidc/src/main/java/org/wildfly/security/http/oidc/QueryParameterTokenRequestAuthenticator.java diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/QueryParameterTokenRequestAuthenticator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/QueryParameterTokenRequestAuthenticator.java new file mode 100644 index 00000000000..7d973144862 --- /dev/null +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/QueryParameterTokenRequestAuthenticator.java @@ -0,0 +1,55 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2022 Red Hat, Inc., and individual 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.wildfly.security.http.oidc; + +/** + * @author Christian Froehlich + * @author Brad Culley + * @author John D. Ament + * @author Farah Juma + */ +public class QueryParameterTokenRequestAuthenticator extends BearerTokenRequestAuthenticator { + public static final String ACCESS_TOKEN = "access_token"; + + public QueryParameterTokenRequestAuthenticator(OidcHttpFacade facade, OidcClientConfiguration oidcClientConfiguration) { + super(facade, oidcClientConfiguration); + } + + public Oidc.AuthOutcome authenticate() { + if(! oidcClientConfiguration.isOAuthQueryParameterEnabled()) { + return Oidc.AuthOutcome.NOT_ATTEMPTED; + } + tokenString = getAccessTokenFromQueryParameter(); + if (tokenString == null || tokenString.trim().isEmpty()) { + challenge = challengeResponse(AuthenticationError.Reason.NO_QUERY_PARAMETER_ACCESS_TOKEN, null, null); + return Oidc.AuthOutcome.NOT_ATTEMPTED; + } + return (verifyToken(tokenString)); + } + + String getAccessTokenFromQueryParameter() { + try { + if (facade != null && facade.getRequest() != null) { + return facade.getRequest().getQueryParamValue(ACCESS_TOKEN); + } + } catch (Exception ignore) { + } + return null; + } +} diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java index 360fac9637f..0cbcb3f3fd5 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java @@ -110,6 +110,24 @@ private AuthOutcome doAuthenticate() { log.debug("Bearer AUTHENTICATED"); return AuthOutcome.AUTHENTICATED; } + + QueryParameterTokenRequestAuthenticator queryParamAuth = new QueryParameterTokenRequestAuthenticator(facade, deployment); + if (log.isTraceEnabled()) { + log.trace("try query parameter auth"); + } + + outcome = queryParamAuth.authenticate(); + if (outcome == AuthOutcome.FAILED) { + challenge = queryParamAuth.getChallenge(); + log.debug("QueryParamAuth auth FAILED"); + return AuthOutcome.FAILED; + } else if (outcome == AuthOutcome.AUTHENTICATED) { + if (verifySSL()) return AuthOutcome.FAILED; + log.debug("QueryParamAuth AUTHENTICATED"); + completeAuthentication(queryParamAuth); + return AuthOutcome.AUTHENTICATED; + } + if (deployment.isBearerOnly()) { challenge = bearer.getChallenge(); log.debug("NOT_ATTEMPTED: bearer only"); From 3fafb911ed2d30a316de4296d8ba83ba6fad88d6 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Mon, 25 Jul 2022 14:49:49 -0400 Subject: [PATCH 05/12] [ELY-2362] Add the ability to retrieve the bearer token using credentials obtained from Basic auth --- .../wildfly/security/http/HttpConstants.java | 1 + .../oidc/BasicAuthRequestAuthenticator.java | 89 +++++++++++++++++++ .../security/http/oidc/ElytronMessages.java | 8 ++ .../org/wildfly/security/http/oidc/Oidc.java | 2 + .../http/oidc/RequestAuthenticator.java | 19 ++++ .../security/http/oidc/ServerRequest.java | 39 ++++++++ 6 files changed, 158 insertions(+) create mode 100644 http/oidc/src/main/java/org/wildfly/security/http/oidc/BasicAuthRequestAuthenticator.java diff --git a/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java b/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java index 0449fb61dd7..1ef8cefec1a 100644 --- a/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java +++ b/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java @@ -155,6 +155,7 @@ private HttpConstants() { public static final String ERROR_DESCRIPTION = "error_description"; public static final String INVALID_TOKEN = "invalid_token"; public static final String STALE_TOKEN = "Stale token"; + public static final String NO_TOKEN = "no_token"; /* * Mechanism Names diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/BasicAuthRequestAuthenticator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/BasicAuthRequestAuthenticator.java new file mode 100644 index 00000000000..03376c83a70 --- /dev/null +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/BasicAuthRequestAuthenticator.java @@ -0,0 +1,89 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2022 Red Hat, Inc., and individual 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.wildfly.security.http.oidc; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.wildfly.common.array.Arrays2.indexOf; +import static org.wildfly.security.http.HttpConstants.NO_TOKEN; +import static org.wildfly.security.http.oidc.ElytronMessages.log; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.util.List; + +import org.wildfly.common.iteration.ByteIterator; +import org.wildfly.security.http.HttpConstants; + +/** + * @author Bill Burke + * @author Farah Juma + */ +public class BasicAuthRequestAuthenticator extends BearerTokenRequestAuthenticator { + + private static final String CHALLENGE_PREFIX = "Basic "; + + public BasicAuthRequestAuthenticator(OidcHttpFacade facade, OidcClientConfiguration oidcClientConfiguration) { + super(facade, oidcClientConfiguration); + } + + public Oidc.AuthOutcome authenticate() { + List authorizationValues = facade.getRequest().getHeaders(HttpConstants.AUTHORIZATION); + if (authorizationValues == null || authorizationValues.isEmpty()) { + challenge = challengeResponse(AuthenticationError.Reason.NO_AUTHORIZATION_HEADER, null, null); + return Oidc.AuthOutcome.NOT_ATTEMPTED; + } + + String basicValue = null; + for (String authorizationValue : authorizationValues) { + if (authorizationValue.regionMatches(true, 0, CHALLENGE_PREFIX, 0, CHALLENGE_PREFIX.length())) { + basicValue = authorizationValue.substring(CHALLENGE_PREFIX.length()); + break; + } + } + if (basicValue == null) { + challenge = challengeResponse(AuthenticationError.Reason.INVALID_TOKEN, null, null); + return Oidc.AuthOutcome.NOT_ATTEMPTED; + } + byte[] decodedValue = ByteIterator.ofBytes(basicValue.getBytes(UTF_8)).asUtf8String().base64Decode().drain(); + int colonPos = indexOf(decodedValue, ':'); + if (colonPos <= 0) { + log.debug("Failed to obtain token"); + challenge = challengeResponse(AuthenticationError.Reason.INVALID_TOKEN, NO_TOKEN, null); + return Oidc.AuthOutcome.FAILED; + } + + ByteBuffer usernameBytes = ByteBuffer.wrap(decodedValue, 0, colonPos); + ByteBuffer passwordBytes = ByteBuffer.wrap(decodedValue, colonPos + 1, decodedValue.length - colonPos - 1); + CharBuffer usernameChars = UTF_8.decode(usernameBytes); + CharBuffer passwordChars = UTF_8.decode(passwordBytes); + AccessAndIDTokenResponse tokenResponse; + try { + String username = usernameChars.toString(); + String password = passwordChars.toString(); + tokenResponse = ServerRequest.getBearerToken(oidcClientConfiguration, username, password); + } catch (Exception e) { + log.debug("Failed to obtain token"); + challenge = challengeResponse(AuthenticationError.Reason.INVALID_TOKEN, NO_TOKEN, e.getMessage()); + return Oidc.AuthOutcome.FAILED; + } + tokenString = tokenResponse.getAccessToken(); + return verifyToken(tokenString); + } + +} diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/ElytronMessages.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/ElytronMessages.java index 89ce19fa64f..c4ba08c8fb2 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/ElytronMessages.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/ElytronMessages.java @@ -22,6 +22,8 @@ import static org.jboss.logging.Logger.Level.WARN; import static org.jboss.logging.annotations.Message.NONE; +import java.io.IOException; + import org.jboss.logging.BasicLogger; import org.jboss.logging.Logger; import org.jboss.logging.annotations.Cause; @@ -225,5 +227,11 @@ interface ElytronMessages extends BasicLogger { @Message(id = 23054, value = "Unexpected value for typ claim") String unexpectedValueForTypeClaim(); + @Message(id = 23055, value = "Unable to obtain token: %d") + IOException unableToObtainToken(int status); + + @Message(id = 23056, value = "No message entity") + IOException noMessageEntity(); + } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java index 39e01421582..1f5925df1ef 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java @@ -63,9 +63,11 @@ public class Oidc { public static final String GRANT_TYPE = "grant_type"; public static final String LOGIN_HINT = "login_hint"; public static final String MAX_AGE = "max_age"; + public static final String PASSWORD = "password"; public static final String PROMPT = "prompt"; public static final String SCOPE = "scope"; public static final String UI_LOCALES = "ui_locales"; + public static final String USERNAME = "username"; public static final String OIDC_SCOPE = "openid"; public static final String REDIRECT_URI = "redirect_uri"; public static final String REFRESH_TOKEN = "refresh_token"; diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java index 0cbcb3f3fd5..438f83b6be1 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java @@ -128,6 +128,25 @@ private AuthOutcome doAuthenticate() { return AuthOutcome.AUTHENTICATED; } + if (deployment.isEnableBasicAuth()) { + BasicAuthRequestAuthenticator basicAuth = new BasicAuthRequestAuthenticator(facade, deployment); + if (log.isTraceEnabled()) { + log.trace("try basic auth"); + } + + outcome = basicAuth.authenticate(); + if (outcome == AuthOutcome.FAILED) { + challenge = basicAuth.getChallenge(); + log.debug("BasicAuth FAILED"); + return AuthOutcome.FAILED; + } else if (outcome == AuthOutcome.AUTHENTICATED) { + if (verifySSL()) return AuthOutcome.FAILED; + log.debug("BasicAuth AUTHENTICATED"); + completeAuthentication(basicAuth); + return AuthOutcome.AUTHENTICATED; + } + } + if (deployment.isBearerOnly()) { challenge = bearer.getChallenge(); log.debug("NOT_ATTEMPTED: bearer only"); diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java index ba9b19e9053..d938cec0a29 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java @@ -18,11 +18,14 @@ package org.wildfly.security.http.oidc; +import static org.wildfly.security.http.oidc.ElytronMessages.log; import static org.wildfly.security.http.oidc.Oidc.AUTHORIZATION_CODE; import static org.wildfly.security.http.oidc.Oidc.CODE; import static org.wildfly.security.http.oidc.Oidc.GRANT_TYPE; import static org.wildfly.security.http.oidc.Oidc.KEYCLOAK_CLIENT_CLUSTER_HOST; +import static org.wildfly.security.http.oidc.Oidc.PASSWORD; import static org.wildfly.security.http.oidc.Oidc.REDIRECT_URI; +import static org.wildfly.security.http.oidc.Oidc.USERNAME; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; @@ -42,6 +45,7 @@ import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; import org.wildfly.security.jose.util.JsonSerialization; /** @@ -241,4 +245,39 @@ public String getError() { } } + public static AccessAndIDTokenResponse getBearerToken(OidcClientConfiguration oidcClientConfiguration, String username, String password) throws Exception { + AccessAndIDTokenResponse tokenResponse; + HttpClient client = oidcClientConfiguration.getClient(); + + HttpPost post = new HttpPost(oidcClientConfiguration.getTokenUrl()); + List formparams = new ArrayList<>(); + formparams.add(new BasicNameValuePair(GRANT_TYPE, PASSWORD)); + formparams.add(new BasicNameValuePair(USERNAME, username)); + formparams.add(new BasicNameValuePair(PASSWORD, password)); + + ClientCredentialsProviderUtils.setClientCredentials(oidcClientConfiguration, post, formparams); + UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); + post.setEntity(form); + + HttpResponse response = client.execute(post); + int status = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + if (status != HttpStatus.SC_OK) { + EntityUtils.consumeQuietly(entity); + throw log.unableToObtainToken(status); + } + if (entity == null) { + throw log.noMessageEntity(); + } + InputStream is = entity.getContent(); + try { + tokenResponse = JsonSerialization.readValue(is, AccessAndIDTokenResponse.class); + } finally { + try { + is.close(); + } catch (java.io.IOException ignored) { + } + } + return tokenResponse; + } } From 875343abb07f6faaea6aa039ea61a8b235ad8ea9 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Mon, 11 Jul 2022 16:08:29 -0400 Subject: [PATCH 06/12] [ELY-2362] Return resource name if client-id hasn't been configured --- .../org/wildfly/security/http/oidc/OidcClientConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java index 250839f12aa..79e10a337f6 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java @@ -142,7 +142,7 @@ public String getResource() { } public String getClientId() { - return clientId; + return clientId != null ? clientId : resource; } public String getRealm() { From 38a29ce7dfc28e19fb6c6df4c56a8d314efd8e63 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Mon, 25 Jul 2022 14:50:04 -0400 Subject: [PATCH 07/12] [ELY-2362] Add tests for bearer-only authentication --- .../security/http/oidc/BearerTest.java | 545 ++++++++++++++++++ .../http/oidc/KeycloakConfiguration.java | 71 ++- .../security/http/oidc/OidcBaseTest.java | 218 +++++++ .../wildfly/security/http/oidc/OidcTest.java | 165 +----- .../http/impl/AbstractBaseHttpTest.java | 83 ++- 5 files changed, 885 insertions(+), 197 deletions(-) create mode 100644 http/oidc/src/test/java/org/wildfly/security/http/oidc/BearerTest.java create mode 100644 http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/BearerTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/BearerTest.java new file mode 100644 index 00000000000..1aacbe3239d --- /dev/null +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/BearerTest.java @@ -0,0 +1,545 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2022 Red Hat, Inc., and individual 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.wildfly.security.http.oidc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; +import static org.wildfly.security.http.oidc.KeycloakConfiguration.ALLOWED_ORIGIN; +import static org.wildfly.security.http.oidc.Oidc.OIDC_NAME; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.http.HttpStatus; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.wildfly.common.iteration.CodePointIterator; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; +import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; + +import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; +import com.gargoylesoftware.htmlunit.HttpMethod; +import com.gargoylesoftware.htmlunit.TextPage; +import com.gargoylesoftware.htmlunit.WebClient; + +import io.restassured.RestAssured; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.QueueDispatcher; +import okhttp3.mockwebserver.RecordedRequest; + +/** + * Tests for bearer only auth. + * + * @author Farah Juma + */ +public class BearerTest extends OidcBaseTest { + + private static boolean DIRECT_ACCESS_GRANT_ENABLED = true; + private static final String BEARER_ONLY_CLIENT_ID = "bearer-client"; + private static final String CORS_CLIENT_ID = "cors-client"; + private static final String SECURED_ENDPOINT = "/service/secured"; + private static final String SECURED_PAGE_TEXT = "Welcome to the secured page!"; + private static final String WRONG_PASSWORD = "WRONG_PASSWORD"; + + protected HttpServerAuthenticationMechanismFactory oidcFactory; + + private enum BearerAuthType { + BEARER, + QUERY_PARAM, + BASIC + } + + @BeforeClass + public static void startTestContainers() throws Exception { + assumeTrue("Docker isn't available, OIDC tests will be skipped", isDockerAvailable()); + KEYCLOAK_CONTAINER = new KeycloakContainer(); + KEYCLOAK_CONTAINER.start(); + sendRealmCreationRequest(KeycloakConfiguration.getRealmRepresentation(TEST_REALM, CLIENT_ID, CLIENT_SECRET, + CLIENT_HOST_NAME, CLIENT_PORT, CLIENT_APP, DIRECT_ACCESS_GRANT_ENABLED, BEARER_ONLY_CLIENT_ID, + CORS_CLIENT_ID)); + client = new MockWebServer(); + client.start(CLIENT_PORT); + } + + private static Dispatcher createAppBearerResponse(HttpServerAuthenticationMechanism mechanism, String clientPageText, + String expectedError, String originHeader) { + return new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest recordedRequest) throws InterruptedException { + String path = recordedRequest.getPath(); + if (path.contains("/" + CLIENT_APP + SECURED_ENDPOINT)) { + try { + String authorizationHeader = recordedRequest.getHeader("Authorization"); + TestingHttpServerRequest request; + if (originHeader != null) { + Map> requestHeaders = new HashMap<>(); + if (authorizationHeader != null) { + requestHeaders.put("Authorization", Collections.singletonList(authorizationHeader)); + } + requestHeaders.put(CorsHeaders.ORIGIN, Collections.singletonList(originHeader)); + request = new TestingHttpServerRequest(requestHeaders, new URI(recordedRequest.getRequestUrl().toString()), recordedRequest.getMethod()); + } else { + request = new TestingHttpServerRequest(authorizationHeader == null ? null : new String[]{authorizationHeader}, + new URI(recordedRequest.getRequestUrl().toString())); + } + mechanism.evaluateRequest(request); + TestingHttpServerResponse response = request.getResponse(); + int statusCode = response.getStatusCode(); + if (expectedError != null) { + assertTrue(response.getAuthenticateHeader().contains(expectedError)); + return new MockResponse().setResponseCode(statusCode); + } else if (statusCode > 300) { + // unexpected error + return new MockResponse().setResponseCode(statusCode); + } + return new MockResponse().setBody(clientPageText); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return new MockResponse() + .setBody(""); + } + }; + } + + @AfterClass + public static void generalCleanup() throws Exception { + if (KEYCLOAK_CONTAINER != null) { + RestAssured + .given() + .auth().oauth2(KeycloakConfiguration.getAdminAccessToken(KEYCLOAK_CONTAINER.getAuthServerUrl())) + .when() + .delete(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/admin/realms/" + TEST_REALM).then().statusCode(204); + KEYCLOAK_CONTAINER.stop(); + } + if (client != null) { + client.shutdown(); + } + } + + @Test + public void testSucessfulAuthenticationWithAuthServerUrl() throws Exception { + performBearerAuthentication(getOidcConfigurationInputStream(), SECURED_ENDPOINT, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + SECURED_PAGE_TEXT); + } + + @Test + public void testSucessfulAuthenticationWithProviderUrl() throws Exception { + performBearerAuthentication(getOidcConfigurationInputStreamWithProviderUrl(), SECURED_ENDPOINT, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + SECURED_PAGE_TEXT); + } + + @Test + public void testWrongToken() throws Exception { + String wrongToken = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJrNmhQYTdHdmdrajdFdlhLeFAtRjFLZkNSUk85Q3kwNC04YzFqTERWOXNrIn0.eyJleHAiOjE2NTc2NjExODksImlhdCI6MTY1NzY2MTEyOSwianRpIjoiZThiZGQ3MWItYTA2OC00Mjc3LTkyY2UtZWJkYmU2MDVkMzBhIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOlsibXlyZWFsbS1yZWFsbSIsIm1hc3Rlci1yZWFsbSIsImFjY291bnQiXSwic3ViIjoiZTliOGE2OWItM2RlNy00ZDYzLWFjYmItMmYyNTRhMDM1MjVkIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoidGVzdC13ZWJhcHAiLCJzZXNzaW9uX3N0YXRlIjoiMTQ1OTdhMmUtOGM1Ni00YzkwLWI3NjAtZWFjYzczNWU1Zjc1IiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjcmVhdGUtcmVhbG0iLCJkZWZhdWx0LXJvbGVzLW1hc3RlciIsIm9mZmxpbmVfYWNjZXNzIiwiYWRtaW4iLCJ1bWFfYXV0aG9yaXphdGlvbiIsInVzZXIiXX0sInJlc291cmNlX2FjY2VzcyI6eyJteXJlYWxtLXJlYWxtIjp7InJvbGVzIjpbInZpZXctcmVhbG0iLCJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJtYXN0ZXItcmVhbG0iOnsicm9sZXMiOlsidmlldy1yZWFsbSIsInZpZXctaWRlbnRpdHktcHJvdmlkZXJzIiwibWFuYWdlLWlkZW50aXR5LXByb3ZpZGVycyIsImltcGVyc29uYXRpb24iLCJjcmVhdGUtY2xpZW50IiwibWFuYWdlLXVzZXJzIiwicXVlcnktcmVhbG1zIiwidmlldy1hdXRob3JpemF0aW9uIiwicXVlcnktY2xpZW50cyIsInF1ZXJ5LXVzZXJzIiwibWFuYWdlLWV2ZW50cyIsIm1hbmFnZS1yZWFsbSIsInZpZXctZXZlbnRzIiwidmlldy11c2VycyIsInZpZXctY2xpZW50cyIsIm1hbmFnZS1hdXRob3JpemF0aW9uIiwibWFuYWdlLWNsaWVudHMiLCJxdWVyeS1ncm91cHMiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6IjE0NTk3YTJlLThjNTYtNGM5MC1iNzYwLWVhY2M3MzVlNWY3NSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxpY2UifQ.hVj6SG-aTcDYhifdljpiBcz4ShCHej3h_4-82rgX0s_oJ-En68Cqt-_DgJLtMdr6dW_gQFFCPYBJfEGvZ8L6b_TwzbdLxyrQrKTOpeG0KJ8VAFlbWum9B1vvES_sav1Gj1sQHlV621EaLISYz7pnknuQEvrB7liJFRRjN9SH30AsAJy6nmKTDHGZ6Eegkveqd_7POaKfsHS3Z0-SGyL5GClXv9yZ1l5Y4VH-rrMUztLPCFH5bJ319-m-7sgizvV-C2EcM37XVAtPRVQbJNRW0wVmLEJKMuLYVnjS1Wn5eU_qnBvVMEaENNG3TzNd6b4YmxMFHFf9tnkb3wkDzdrRTA"; + performBearerAuthentication(getOidcConfigurationInputStreamWithProviderUrl(), SECURED_ENDPOINT, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + SECURED_PAGE_TEXT, wrongToken, BearerAuthType.BEARER); + } + + @Test + public void testInvalidToken() throws Exception { + performBearerAuthentication(getOidcConfigurationInputStreamWithProviderUrl(), SECURED_ENDPOINT, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + SECURED_PAGE_TEXT, "INVALID_TOKEN", BearerAuthType.BEARER); + } + + @Test + public void testNoTokenProvidedWithAuthServerUrl() throws Exception { + accessAppWithoutToken(SECURED_ENDPOINT, getOidcConfigurationInputStream()); + } + + @Test + public void testNoTokenProvidedWithProviderUrl() throws Exception { + accessAppWithoutToken(SECURED_ENDPOINT, getOidcConfigurationInputStreamWithProviderUrl()); + } + + @Test + public void testTokenProvidedBearerOnlyNotSet() throws Exception { + // ensure we still make use of the bearer token + performBearerAuthentication(getOidcConfigurationInputStreamWithoutBearerOnly(), SECURED_ENDPOINT, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + SECURED_PAGE_TEXT); + } + + @Test + public void testTokenNotProvidedBearerOnlyNotSet() throws Exception { + // ensure the regular OIDC flow takes place + accessAppWithoutToken("", getRegularOidcConfigurationInputStream()); + } + + /** + * Tests that pass the bearer token to use via an access_token query param. + */ + + @Test + public void testValidTokenViaQueryParameter() throws Exception { + performBearerAuthentication(getOidcConfigurationInputStreamWithProviderUrl(), SECURED_ENDPOINT, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + SECURED_PAGE_TEXT, null, BearerAuthType.QUERY_PARAM); + } + + @Test + public void testWrongTokenViaQueryParameter() throws Exception { + String wrongToken = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJrNmhQYTdHdmdrajdFdlhLeFAtRjFLZkNSUk85Q3kwNC04YzFqTERWOXNrIn0.eyJleHAiOjE2NTc2NjExODksImlhdCI6MTY1NzY2MTEyOSwianRpIjoiZThiZGQ3MWItYTA2OC00Mjc3LTkyY2UtZWJkYmU2MDVkMzBhIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOlsibXlyZWFsbS1yZWFsbSIsIm1hc3Rlci1yZWFsbSIsImFjY291bnQiXSwic3ViIjoiZTliOGE2OWItM2RlNy00ZDYzLWFjYmItMmYyNTRhMDM1MjVkIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoidGVzdC13ZWJhcHAiLCJzZXNzaW9uX3N0YXRlIjoiMTQ1OTdhMmUtOGM1Ni00YzkwLWI3NjAtZWFjYzczNWU1Zjc1IiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjcmVhdGUtcmVhbG0iLCJkZWZhdWx0LXJvbGVzLW1hc3RlciIsIm9mZmxpbmVfYWNjZXNzIiwiYWRtaW4iLCJ1bWFfYXV0aG9yaXphdGlvbiIsInVzZXIiXX0sInJlc291cmNlX2FjY2VzcyI6eyJteXJlYWxtLXJlYWxtIjp7InJvbGVzIjpbInZpZXctcmVhbG0iLCJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJtYXN0ZXItcmVhbG0iOnsicm9sZXMiOlsidmlldy1yZWFsbSIsInZpZXctaWRlbnRpdHktcHJvdmlkZXJzIiwibWFuYWdlLWlkZW50aXR5LXByb3ZpZGVycyIsImltcGVyc29uYXRpb24iLCJjcmVhdGUtY2xpZW50IiwibWFuYWdlLXVzZXJzIiwicXVlcnktcmVhbG1zIiwidmlldy1hdXRob3JpemF0aW9uIiwicXVlcnktY2xpZW50cyIsInF1ZXJ5LXVzZXJzIiwibWFuYWdlLWV2ZW50cyIsIm1hbmFnZS1yZWFsbSIsInZpZXctZXZlbnRzIiwidmlldy11c2VycyIsInZpZXctY2xpZW50cyIsIm1hbmFnZS1hdXRob3JpemF0aW9uIiwibWFuYWdlLWNsaWVudHMiLCJxdWVyeS1ncm91cHMiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6IjE0NTk3YTJlLThjNTYtNGM5MC1iNzYwLWVhY2M3MzVlNWY3NSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxpY2UifQ.hVj6SG-aTcDYhifdljpiBcz4ShCHej3h_4-82rgX0s_oJ-En68Cqt-_DgJLtMdr6dW_gQFFCPYBJfEGvZ8L6b_TwzbdLxyrQrKTOpeG0KJ8VAFlbWum9B1vvES_sav1Gj1sQHlV621EaLISYz7pnknuQEvrB7liJFRRjN9SH30AsAJy6nmKTDHGZ6Eegkveqd_7POaKfsHS3Z0-SGyL5GClXv9yZ1l5Y4VH-rrMUztLPCFH5bJ319-m-7sgizvV-C2EcM37XVAtPRVQbJNRW0wVmLEJKMuLYVnjS1Wn5eU_qnBvVMEaENNG3TzNd6b4YmxMFHFf9tnkb3wkDzdrRTA"; + performBearerAuthentication(getOidcConfigurationInputStreamWithProviderUrl(), SECURED_ENDPOINT, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + SECURED_PAGE_TEXT, wrongToken, BearerAuthType.QUERY_PARAM); + } + + @Test + public void testInvalidTokenViaQueryParameter() throws Exception { + performBearerAuthentication(getOidcConfigurationInputStreamWithProviderUrl(), SECURED_ENDPOINT, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + SECURED_PAGE_TEXT, "INVALID_TOKEN", BearerAuthType.QUERY_PARAM); + } + + /** + * Tests that rely on obtaining the bearer token to use from credentials obtained from basic auth. + */ + + @Test + public void testBasicAuthenticationWithoutEnableBasicAuthSet() throws Exception { + accessAppWithoutToken(SECURED_ENDPOINT, getOidcConfigurationInputStream(), BearerAuthType.BASIC, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD); + } + + @Test + public void testBasicAuthenticationWithoutEnableBasicAuthSetAndWithoutBearerOnlySet() throws Exception { + // ensure the regular OIDC flow takes place + accessAppWithoutToken("", getRegularOidcConfigurationInputStream(), BearerAuthType.BASIC, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD); + } + + @Test + public void testValidCredentialsBasicAuthentication() throws Exception { + performBearerAuthentication(getOidcConfigurationInputStreamWithEnableBasicAuth(), SECURED_ENDPOINT, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + SECURED_PAGE_TEXT, null, BearerAuthType.BASIC); + } + + @Test + public void testInvalidCredentialsBasicAuthentication() throws Exception { + accessAppWithoutToken(SECURED_ENDPOINT, getOidcConfigurationInputStreamWithEnableBasicAuth(), BearerAuthType.BASIC, KeycloakConfiguration.ALICE, WRONG_PASSWORD); + } + + /** + * Tests that simulate CORS preflight requests. + */ + + @Test + public void testCorsRequestWithEnableCors() throws Exception { + performBearerAuthenticationCorsRequest(getOidcConfigurationInputStreamWithEnableCors(), SECURED_ENDPOINT, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + SECURED_PAGE_TEXT, null, ALLOWED_ORIGIN); + } + + @Test + public void testCorsRequestWithEnableCorsWithWrongToken() throws Exception { + String wrongToken = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJrNmhQYTdHdmdrajdFdlhLeFAtRjFLZkNSUk85Q3kwNC04YzFqTERWOXNrIn0.eyJleHAiOjE2NTc2NjExODksImlhdCI6MTY1NzY2MTEyOSwianRpIjoiZThiZGQ3MWItYTA2OC00Mjc3LTkyY2UtZWJkYmU2MDVkMzBhIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOlsibXlyZWFsbS1yZWFsbSIsIm1hc3Rlci1yZWFsbSIsImFjY291bnQiXSwic3ViIjoiZTliOGE2OWItM2RlNy00ZDYzLWFjYmItMmYyNTRhMDM1MjVkIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoidGVzdC13ZWJhcHAiLCJzZXNzaW9uX3N0YXRlIjoiMTQ1OTdhMmUtOGM1Ni00YzkwLWI3NjAtZWFjYzczNWU1Zjc1IiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjcmVhdGUtcmVhbG0iLCJkZWZhdWx0LXJvbGVzLW1hc3RlciIsIm9mZmxpbmVfYWNjZXNzIiwiYWRtaW4iLCJ1bWFfYXV0aG9yaXphdGlvbiIsInVzZXIiXX0sInJlc291cmNlX2FjY2VzcyI6eyJteXJlYWxtLXJlYWxtIjp7InJvbGVzIjpbInZpZXctcmVhbG0iLCJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJtYXN0ZXItcmVhbG0iOnsicm9sZXMiOlsidmlldy1yZWFsbSIsInZpZXctaWRlbnRpdHktcHJvdmlkZXJzIiwibWFuYWdlLWlkZW50aXR5LXByb3ZpZGVycyIsImltcGVyc29uYXRpb24iLCJjcmVhdGUtY2xpZW50IiwibWFuYWdlLXVzZXJzIiwicXVlcnktcmVhbG1zIiwidmlldy1hdXRob3JpemF0aW9uIiwicXVlcnktY2xpZW50cyIsInF1ZXJ5LXVzZXJzIiwibWFuYWdlLWV2ZW50cyIsIm1hbmFnZS1yZWFsbSIsInZpZXctZXZlbnRzIiwidmlldy11c2VycyIsInZpZXctY2xpZW50cyIsIm1hbmFnZS1hdXRob3JpemF0aW9uIiwibWFuYWdlLWNsaWVudHMiLCJxdWVyeS1ncm91cHMiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6IjE0NTk3YTJlLThjNTYtNGM5MC1iNzYwLWVhY2M3MzVlNWY3NSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxpY2UifQ.hVj6SG-aTcDYhifdljpiBcz4ShCHej3h_4-82rgX0s_oJ-En68Cqt-_DgJLtMdr6dW_gQFFCPYBJfEGvZ8L6b_TwzbdLxyrQrKTOpeG0KJ8VAFlbWum9B1vvES_sav1Gj1sQHlV621EaLISYz7pnknuQEvrB7liJFRRjN9SH30AsAJy6nmKTDHGZ6Eegkveqd_7POaKfsHS3Z0-SGyL5GClXv9yZ1l5Y4VH-rrMUztLPCFH5bJ319-m-7sgizvV-C2EcM37XVAtPRVQbJNRW0wVmLEJKMuLYVnjS1Wn5eU_qnBvVMEaENNG3TzNd6b4YmxMFHFf9tnkb3wkDzdrRTA"; + performBearerAuthenticationCorsRequest(getOidcConfigurationInputStreamWithEnableCors(), SECURED_ENDPOINT, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + SECURED_PAGE_TEXT, wrongToken, ALLOWED_ORIGIN); + } + + @Test + public void testCorsRequestWithEnableCorsWithInvalidToken() throws Exception { + performBearerAuthenticationCorsRequest(getOidcConfigurationInputStreamWithEnableCors(), SECURED_ENDPOINT, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + SECURED_PAGE_TEXT, "INVALID_TOKEN", ALLOWED_ORIGIN); + } + + @Test + public void testCorsRequestWithEnableCorsInvalidOrigin() throws Exception { + performBearerAuthenticationCorsRequest(getOidcConfigurationInputStreamWithEnableCors(), SECURED_ENDPOINT, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + SECURED_PAGE_TEXT, null, "http://invalidorigin"); + } + + @Test + public void testCorsRequestWithoutEnableCors() throws Exception { + performBearerAuthenticationCorsRequest(getOidcConfigurationInputStream(), SECURED_ENDPOINT, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + SECURED_PAGE_TEXT, null, ALLOWED_ORIGIN); + } + + private void performBearerAuthentication(InputStream oidcConfig, String endpoint, String username, String password, String clientPageText) throws Exception { + performBearerAuthentication(oidcConfig, endpoint, username, password, clientPageText, null, BearerAuthType.BEARER); + } + + private void performBearerAuthentication(InputStream oidcConfig, String endpoint, String username, String password, + String clientPageText, String bearerToken, BearerAuthType bearerAuthType) throws Exception { + try { + Map props = new HashMap<>(); + OidcClientConfiguration oidcClientConfiguration = OidcClientConfigurationBuilder.build(oidcConfig); + assertEquals(OidcClientConfiguration.RelativeUrlsUsed.NEVER, oidcClientConfiguration.getRelativeUrls()); + + OidcClientContext oidcClientContext = new OidcClientContext(oidcClientConfiguration); + oidcFactory = new OidcMechanismFactory(oidcClientContext); + HttpServerAuthenticationMechanism mechanism = oidcFactory.createAuthenticationMechanism(OIDC_NAME, props, getCallbackHandler()); + + if (bearerToken != null) { // going to pass an invalid token + client.setDispatcher(createAppBearerResponse(mechanism, clientPageText, "invalid_token", null)); + } else { + client.setDispatcher(createAppBearerResponse(mechanism, clientPageText, null, null)); + } + + URI requestUri; + WebClient webClient = getWebClient(); + switch (bearerAuthType) { + case QUERY_PARAM: + if (bearerToken == null) { + // obtain a bearer token and then try accessing the endpoint with a query param specified + requestUri = new URI(getClientUrl() + endpoint + "?access_token=" + + KeycloakConfiguration.getAccessToken(KEYCLOAK_CONTAINER.getAuthServerUrl(), TEST_REALM, username, + password, CLIENT_ID, CLIENT_SECRET)); + } else { + // try accessing the endpoint with the given bearer token specified using a query param + requestUri = new URI(getClientUrl() + endpoint + "?access_token=" + bearerToken); + } + break; + case BASIC: + webClient.addRequestHeader("Authorization", + "Basic " + CodePointIterator.ofString(username + ":" + password).asUtf8().base64Encode().drainToString()); + requestUri = new URI(getClientUrl() + endpoint); + break; + default: + if (bearerToken == null) { + // obtain a bearer token and then try accessing the endpoint with the Authorization header specified + webClient.addRequestHeader("Authorization", "Bearer " + KeycloakConfiguration.getAccessToken(KEYCLOAK_CONTAINER.getAuthServerUrl(), TEST_REALM, username, + password, CLIENT_ID, CLIENT_SECRET)); + } else { + // try accessing the endpoint with the given bearer token specified using the Authorization header + webClient.addRequestHeader("Authorization", "Bearer " + bearerToken); + } + requestUri = new URI(getClientUrl() + endpoint); + } + + if (bearerToken == null) { + TextPage page = webClient.getPage(requestUri.toURL()); + assertEquals(HttpStatus.SC_OK, page.getWebResponse().getStatusCode()); + assertTrue(page.getContent().contains(clientPageText)); + } else { + try { + webClient.getPage(requestUri.toURL()); + fail("Expected exception not thrown"); + } catch (FailingHttpStatusCodeException e) { + assertEquals(HttpStatus.SC_UNAUTHORIZED, e.getStatusCode()); + } + } + } finally { + client.setDispatcher(new QueueDispatcher()); + } + } + + private void performBearerAuthenticationCorsRequest(InputStream oidcConfig, String endpoint, String username, String password, + String clientPageText, String bearerToken, String originHeader) throws Exception { + try { + Map props = new HashMap<>(); + OidcClientConfiguration oidcClientConfiguration = OidcClientConfigurationBuilder.build(oidcConfig); + assertEquals(OidcClientConfiguration.RelativeUrlsUsed.NEVER, oidcClientConfiguration.getRelativeUrls()); + + OidcClientContext oidcClientContext = new OidcClientContext(oidcClientConfiguration); + oidcFactory = new OidcMechanismFactory(oidcClientContext); + HttpServerAuthenticationMechanism mechanism = oidcFactory.createAuthenticationMechanism(OIDC_NAME, props, getCallbackHandler()); + + URI requestUri = new URI(getClientUrl() + endpoint); + + // simulate preflight request + Map> requestHeaders = new HashMap<>(); + requestHeaders.put(CorsHeaders.ORIGIN, Collections.singletonList(originHeader)); + requestHeaders.put(CorsHeaders.ACCESS_CONTROL_REQUEST_HEADERS, Collections.singletonList("authorization")); + requestHeaders.put(CorsHeaders.ACCESS_CONTROL_REQUEST_METHOD, Collections.singletonList(HttpMethod.GET.name())); + TestingHttpServerRequest request = new TestingHttpServerRequest(requestHeaders, requestUri, HttpMethod.OPTIONS.name()); + mechanism.evaluateRequest(request); + TestingHttpServerResponse response = request.getResponse(); + + if (oidcClientConfiguration.isCors()) { + assertTrue(Boolean.valueOf(response.getFirstResponseHeaderValue(CorsHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS))); + assertEquals("authorization", response.getFirstResponseHeaderValue(CorsHeaders.ACCESS_CONTROL_ALLOW_HEADERS)); + assertEquals(HttpMethod.GET.name(), response.getFirstResponseHeaderValue(CorsHeaders.ACCESS_CONTROL_ALLOW_METHODS)); + assertEquals(originHeader, response.getFirstResponseHeaderValue(CorsHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + + if (bearerToken != null) { // going to pass an invalid token + client.setDispatcher(createAppBearerResponse(mechanism, clientPageText, "invalid_token", originHeader)); + } else { + client.setDispatcher(createAppBearerResponse(mechanism, clientPageText, null, originHeader)); + } + + WebClient webClient = getWebClient(); + webClient.addRequestHeader(CorsHeaders.ORIGIN, originHeader); + if (bearerToken == null) { + webClient.addRequestHeader("Authorization", "Bearer " + KeycloakConfiguration.getAccessToken(KEYCLOAK_CONTAINER.getAuthServerUrl(), TEST_REALM, username, + password, CORS_CLIENT_ID, CLIENT_SECRET)); + } else { + webClient.addRequestHeader("Authorization", "Bearer " + bearerToken); + } + if (bearerToken == null) { + try { + TextPage page = webClient.getPage(requestUri.toURL()); + assertEquals(HttpStatus.SC_OK, page.getWebResponse().getStatusCode()); + assertTrue(page.getContent().contains(clientPageText)); + } catch (FailingHttpStatusCodeException e) { + assertFalse(originHeader.equals(ALLOWED_ORIGIN)); + assertEquals(HttpStatus.SC_FORBIDDEN, e.getStatusCode()); + } + } else { + try { + webClient.getPage(requestUri.toURL()); + fail("Expected exception not thrown"); + } catch (FailingHttpStatusCodeException e) { + assertEquals(HttpStatus.SC_UNAUTHORIZED, e.getStatusCode()); + } + } + } else { + assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); + if (oidcClientConfiguration.getRealm() != null) { + // if we have a keycloak realm configured, its name should appear in the challenge + assertEquals("Bearer realm=\"" + TEST_REALM + "\"", response.getAuthenticateHeader()); + } else { + assertEquals("Bearer", response.getAuthenticateHeader()); + } + } + } finally { + client.setDispatcher(new QueueDispatcher()); + } + } + + private void accessAppWithoutToken(String endpoint, InputStream oidcConfigInputStream) throws Exception { + accessAppWithoutToken(endpoint, oidcConfigInputStream, null, null, null); + } + + private void accessAppWithoutToken(String endpoint, InputStream oidcConfigInputStream, BearerAuthType bearerAuthType, String username, String password) throws Exception { + Map props = new HashMap<>(); + OidcClientConfiguration oidcClientConfiguration = OidcClientConfigurationBuilder.build(oidcConfigInputStream); + assertEquals(OidcClientConfiguration.RelativeUrlsUsed.NEVER, oidcClientConfiguration.getRelativeUrls()); + + OidcClientContext oidcClientContext = new OidcClientContext(oidcClientConfiguration); + oidcFactory = new OidcMechanismFactory(oidcClientContext); + HttpServerAuthenticationMechanism mechanism = oidcFactory.createAuthenticationMechanism(OIDC_NAME, props, getCallbackHandler()); + + URI requestUri = new URI(getClientUrl() + endpoint); + TestingHttpServerRequest request; + if (bearerAuthType == BearerAuthType.BASIC) { + request = new TestingHttpServerRequest(new String[] {"Basic " + + CodePointIterator.ofString(username + ":" + password).asUtf8().base64Encode().drainToString()}, requestUri); + } else { + request = new TestingHttpServerRequest(null, requestUri); // no bearer token specified + } + mechanism.evaluateRequest(request); + TestingHttpServerResponse response = request.getResponse(); + + if (oidcClientConfiguration.isBearerOnly() || oidcClientConfiguration.isEnableBasicAuth()) { + assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); + String authenticateHeader = response.getAuthenticateHeader(); + if ((bearerAuthType == BearerAuthType.BASIC) && password.equals(WRONG_PASSWORD)) { + assertTrue(authenticateHeader.startsWith("Bearer error=\"" + "no_token" + "\"")); + assertTrue(authenticateHeader.contains("error_description")); + assertTrue(authenticateHeader.contains(String.valueOf(HttpStatus.SC_UNAUTHORIZED))); + } else if (oidcClientConfiguration.getRealm() != null) { + // if we have a keycloak realm configured, its name should appear in the challenge + assertEquals("Bearer realm=\"" + TEST_REALM + "\"", authenticateHeader); + } else { + assertEquals("Bearer", authenticateHeader); + } + } else { + // no token provided and bearer-only is not configured, should end up in the OIDC flow + assertEquals(HttpStatus.SC_MOVED_TEMPORARILY, response.getStatusCode()); + assertEquals(Status.NO_AUTH, request.getResult()); + try { + // browser login should succeed + client.setDispatcher(createAppResponse(mechanism, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT)); + TextPage page = loginToKeycloak(KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, requestUri, response.getLocation(), + response.getCookies()).click(); + assertTrue(page.getContent().contains(CLIENT_PAGE_TEXT)); + } finally { + client.setDispatcher(new QueueDispatcher()); + } + } + } + + private InputStream getOidcConfigurationInputStream() { + return getOidcConfigurationInputStream(KEYCLOAK_CONTAINER.getAuthServerUrl()); + } + + private InputStream getOidcConfigurationInputStream(String authServerUrl) { + String oidcConfig = "{\n" + + " \"realm\" : \"" + TEST_REALM + "\",\n" + + " \"resource\" : \"" + BEARER_ONLY_CLIENT_ID + "\",\n" + + " \"auth-server-url\" : \"" + authServerUrl + "\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"bearer-only\" : \"true\"\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } + + private InputStream getOidcConfigurationInputStreamWithProviderUrl() { + String oidcConfig = "{\n" + + " \"client-id\" : \"" + BEARER_ONLY_CLIENT_ID + "\",\n" + + " \"provider-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM + "\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"bearer-only\" : \"true\"\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } + + private InputStream getOidcConfigurationInputStreamWithoutBearerOnly() { + String oidcConfig = "{\n" + + " \"client-id\" : \"" + BEARER_ONLY_CLIENT_ID + "\",\n" + + " \"provider-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM + "\",\n" + + " \"ssl-required\" : \"EXTERNAL\"\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } + + private InputStream getRegularOidcConfigurationInputStream() { + String oidcConfig = "{\n" + + " \"client-id\" : \"" + CLIENT_ID + "\",\n" + + " \"provider-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM + "\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"credentials\" : {\n" + + " \"secret\" : \"" + CLIENT_SECRET + "\"\n" + + " }\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } + + private InputStream getOidcConfigurationInputStreamWithEnableBasicAuth() { + String oidcConfig = "{\n" + + " \"client-id\" : \"" + CLIENT_ID + "\",\n" + + " \"provider-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM + "\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"enable-basic-auth\" : \"true\",\n" + + " \"credentials\" : {\n" + + " \"secret\" : \"" + CLIENT_SECRET + "\"\n" + + " }\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } + + private InputStream getOidcConfigurationInputStreamWithEnableCors() { + String oidcConfig = "{\n" + + " \"client-id\" : \"" + BEARER_ONLY_CLIENT_ID + "\",\n" + + " \"provider-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM + "\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"enable-cors\" : \"true\",\n" + + " \"bearer-only\" : \"true\"\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } +} \ No newline at end of file diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/KeycloakConfiguration.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/KeycloakConfiguration.java index 0e80a70cf59..5dfa052ed28 100644 --- a/http/oidc/src/test/java/org/wildfly/security/http/oidc/KeycloakConfiguration.java +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/KeycloakConfiguration.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.keycloak.representations.AccessTokenResponse; @@ -45,6 +46,7 @@ public class KeycloakConfiguration { public static final String ALICE_PASSWORD = "alice123+"; private static final String BOB = "bob"; private static final String BOB_PASSWORD = "bob123+"; + public static final String ALLOWED_ORIGIN = "http://somehost"; /** * Configure RealmRepresentation as follows: @@ -62,20 +64,52 @@ public static RealmRepresentation getRealmRepresentation(final String realmName, return createRealm(realmName, clientId, clientSecret, clientHostName, clientPort, clientApp); } + public static RealmRepresentation getRealmRepresentation(final String realmName, String clientId, String clientSecret, + String clientHostName, int clientPort, String clientApp, + boolean directAccessGrantEnabled, String bearerOnlyClientId, + String corsClientId) { + return createRealm(realmName, clientId, clientSecret, clientHostName, clientPort, clientApp, directAccessGrantEnabled, bearerOnlyClientId, corsClientId); + } + public static String getAdminAccessToken(String authServerUrl) { + return getAdminAccessToken(authServerUrl, "master", KeycloakContainer.KEYCLOAK_ADMIN_USER, + KeycloakContainer.KEYCLOAK_ADMIN_PASSWORD, "admin-cli"); + } + + public static String getAdminAccessToken(String authServerUrl, String realmName, String username, String password, String clientId) { return RestAssured .given() .param("grant_type", "password") - .param("username", KeycloakContainer.KEYCLOAK_ADMIN_USER) - .param("password", KeycloakContainer.KEYCLOAK_ADMIN_PASSWORD) - .param("client_id", "admin-cli") + .param("username", username) + .param("password", password) + .param("client_id", clientId) .when() - .post(authServerUrl + "/realms/master/protocol/openid-connect/token") + .post(authServerUrl + "/realms/" + realmName + "/protocol/openid-connect/token") + .as(AccessTokenResponse.class).getToken(); + } + + public static String getAccessToken(String authServerUrl, String realmName, String username, String password, String clientId, String clientSecret) { + return RestAssured + .given() + .param("grant_type", "password") + .param("username", username) + .param("password", password) + .param("client_id", clientId) + .param("client_secret", clientSecret) + .when() + .post(authServerUrl + "/realms/" + realmName + "/protocol/openid-connect/token") .as(AccessTokenResponse.class).getToken(); } private static RealmRepresentation createRealm(String name, String clientId, String clientSecret, String clientHostName, int clientPort, String clientApp) { + return createRealm(name, clientId, clientSecret, clientHostName, clientPort, clientApp, false, null, null); + } + + private static RealmRepresentation createRealm(String name, String clientId, String clientSecret, + String clientHostName, int clientPort, String clientApp, + boolean directAccessGrantEnabled, String bearerOnlyClientId, + String corsClientId) { RealmRepresentation realm = new RealmRepresentation(); realm.setRealm(name); @@ -94,14 +128,27 @@ private static RealmRepresentation createRealm(String name, String clientId, Str realm.getRoles().getRealm().add(new RoleRepresentation("user", null, false)); realm.getRoles().getRealm().add(new RoleRepresentation("admin", null, false)); - realm.getClients().add(createWebAppClient(clientId, clientSecret, clientHostName, clientPort, clientApp)); + realm.getClients().add(createWebAppClient(clientId, clientSecret, clientHostName, clientPort, clientApp, directAccessGrantEnabled)); + + if (bearerOnlyClientId != null) { + realm.getClients().add(createBearerOnlyClient(bearerOnlyClientId)); + } + + if (corsClientId != null) { + realm.getClients().add(createWebAppClient(corsClientId, clientSecret, clientHostName, clientPort, clientApp, directAccessGrantEnabled, ALLOWED_ORIGIN)); + } realm.getUsers().add(createUser(ALICE, ALICE_PASSWORD, Arrays.asList(USER_ROLE, ADMIN_ROLE))); realm.getUsers().add(createUser(BOB, BOB_PASSWORD, Arrays.asList(USER_ROLE))); return realm; } - private static ClientRepresentation createWebAppClient(String clientId, String clientSecret, String clientHostName, int clientPort, String clientApp) { + private static ClientRepresentation createWebAppClient(String clientId, String clientSecret, String clientHostName, int clientPort, String clientApp, boolean directAccessGrantEnabled) { + return createWebAppClient(clientId, clientSecret, clientHostName, clientPort, clientApp, directAccessGrantEnabled, null); + } + + private static ClientRepresentation createWebAppClient(String clientId, String clientSecret, String clientHostName, int clientPort, + String clientApp, boolean directAccessGrantEnabled, String allowedOrigin) { ClientRepresentation client = new ClientRepresentation(); client.setClientId(clientId); client.setPublicClient(false); @@ -109,6 +156,18 @@ private static ClientRepresentation createWebAppClient(String clientId, String c //client.setRedirectUris(Arrays.asList("*")); client.setRedirectUris(Arrays.asList("http://" + clientHostName + ":" + clientPort + "/" + clientApp)); client.setEnabled(true); + client.setDirectAccessGrantsEnabled(directAccessGrantEnabled); + if (allowedOrigin != null) { + client.setWebOrigins(Collections.singletonList(allowedOrigin)); + } + return client; + } + + private static ClientRepresentation createBearerOnlyClient(String clientId) { + ClientRepresentation client = new ClientRepresentation(); + client.setClientId(clientId); + client.setBearerOnly(true); + client.setEnabled(true); return client; } diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java new file mode 100644 index 00000000000..b1fb8ea2d2e --- /dev/null +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java @@ -0,0 +1,218 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2022 Red Hat, Inc., and individual 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.wildfly.security.http.oidc; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.net.URI; +import java.util.List; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.AuthorizeCallback; + +import org.junit.AfterClass; +import org.keycloak.representations.idm.RealmRepresentation; +import org.testcontainers.DockerClientFactory; +import org.wildfly.security.auth.callback.AuthenticationCompleteCallback; +import org.wildfly.security.auth.callback.EvidenceVerifyCallback; +import org.wildfly.security.auth.callback.IdentityCredentialCallback; +import org.wildfly.security.auth.callback.SecurityIdentityCallback; +import org.wildfly.security.auth.server.SecurityDomain; +import org.wildfly.security.evidence.Evidence; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; +import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; +import org.wildfly.security.http.HttpServerCookie; +import org.wildfly.security.http.impl.AbstractBaseHttpTest; +import org.wildfly.security.jose.util.JsonSerialization; + +import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlInput; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.javascript.SilentJavaScriptErrorListener; + +import io.restassured.RestAssured; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; + +/** + * Tests for the OpenID Connect authentication mechanism. + * + * @author Farah Juma + */ +public class OidcBaseTest extends AbstractBaseHttpTest { + + public static final String CLIENT_ID = "test-webapp"; + public static final String CLIENT_SECRET = "secret"; + public static KeycloakContainer KEYCLOAK_CONTAINER; + public static final String TEST_REALM = "WildFly"; + public static final String KEYCLOAK_USERNAME = "username"; + public static final String KEYCLOAK_PASSWORD = "password"; + public static final String KEYCLOAK_LOGIN = "login"; + public static final int CLIENT_PORT = 5002; + public static final String CLIENT_APP = "clientApp"; + public static final String CLIENT_PAGE_TEXT = "Welcome page!"; + public static final String CLIENT_HOST_NAME = "localhost"; + public static MockWebServer client; // to simulate the application being secured + + protected HttpServerAuthenticationMechanismFactory oidcFactory; + + @AfterClass + public static void generalCleanup() throws Exception { + if (KEYCLOAK_CONTAINER != null) { + RestAssured + .given() + .auth().oauth2(KeycloakConfiguration.getAdminAccessToken(KEYCLOAK_CONTAINER.getAuthServerUrl())) + .when() + .delete(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/admin/realms/" + TEST_REALM).then().statusCode(204); + KEYCLOAK_CONTAINER.stop(); + } + if (client != null) { + client.shutdown(); + } + } + + protected static void sendRealmCreationRequest(RealmRepresentation realm) { + try { + RestAssured + .given() + .auth().oauth2(KeycloakConfiguration.getAdminAccessToken(KEYCLOAK_CONTAINER.getAuthServerUrl())) + .contentType("application/json") + .body(JsonSerialization.writeValueAsBytes(realm)) + .when() + .post(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/admin/realms").then() + .statusCode(201); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected static boolean isDockerAvailable() { + try { + DockerClientFactory.instance().client(); + return true; + } catch (Throwable ex) { + return false; + } + } + + protected CallbackHandler getCallbackHandler() { + return callbacks -> { + for(Callback callback : callbacks) { + if (callback instanceof EvidenceVerifyCallback) { + Evidence evidence = ((EvidenceVerifyCallback) callback).getEvidence(); + ((EvidenceVerifyCallback) callback).setVerified(evidence.getDecodedPrincipal() != null); + } else if (callback instanceof AuthenticationCompleteCallback) { + // NO-OP + } else if (callback instanceof IdentityCredentialCallback) { + // NO-OP + } else if (callback instanceof AuthorizeCallback) { + ((AuthorizeCallback) callback).setAuthorized(true); + } else if (callback instanceof SecurityIdentityCallback) { + ((SecurityIdentityCallback) callback).setSecurityIdentity(SecurityDomain.builder().build().getCurrentSecurityIdentity()); + } else { + throw new UnsupportedCallbackException(callback); + } + } + }; + } + + protected static Dispatcher createAppResponse(HttpServerAuthenticationMechanism mechanism, int expectedStatusCode, String expectedLocation, String clientPageText) { + return new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest recordedRequest) throws InterruptedException { + String path = recordedRequest.getPath(); + if (path.contains("/" + CLIENT_APP) && path.contains("&code=")) { + try { + TestingHttpServerRequest request = new TestingHttpServerRequest(new String[0], + new URI(recordedRequest.getRequestUrl().toString()), recordedRequest.getHeader("Cookie")); + mechanism.evaluateRequest(request); + TestingHttpServerResponse response = request.getResponse(); + assertEquals(expectedStatusCode, response.getStatusCode()); + assertEquals(expectedLocation, response.getLocation()); + return new MockResponse().setBody(clientPageText); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return new MockResponse() + .setBody(""); + } + }; + } + + protected WebClient getWebClient() { + WebClient webClient = new WebClient(); + webClient.setCssErrorHandler(new SilentCssErrorHandler()); + webClient.setJavaScriptErrorListener(new SilentJavaScriptErrorListener()); + return webClient; + } + + protected static String getClientUrl() { + return "http://" + CLIENT_HOST_NAME + ":" + CLIENT_PORT + "/" + CLIENT_APP; + } + + protected HtmlInput loginToKeycloak(String username, String password, URI requestUri, String location, List cookies) throws IOException { + WebClient webClient = getWebClient(); + if (cookies != null) { + for (HttpServerCookie cookie : cookies) { + webClient.addCookie(getCookieString(cookie), requestUri.toURL(), null); + } + } + HtmlPage keycloakLoginPage = webClient.getPage(location); + HtmlForm loginForm = keycloakLoginPage.getForms().get(0); + loginForm.getInputByName(KEYCLOAK_USERNAME).setValueAttribute(username); + loginForm.getInputByName(KEYCLOAK_PASSWORD).setValueAttribute(password); + return loginForm.getInputByName(KEYCLOAK_LOGIN); + } + + protected String getCookieString(HttpServerCookie cookie) { + final StringBuilder header = new StringBuilder(cookie.getName()); + header.append("="); + if(cookie.getValue() != null) { + header.append(cookie.getValue()); + } + if (cookie.getPath() != null) { + header.append("; Path="); + header.append(cookie.getPath()); + } + if (cookie.getDomain() != null) { + header.append("; Domain="); + header.append(cookie.getDomain()); + } + if (cookie.isSecure()) { + header.append("; Secure"); + } + if (cookie.isHttpOnly()) { + header.append("; HttpOnly"); + } + if (cookie.getMaxAge() >= 0) { + header.append("; Max-Age="); + header.append(cookie.getMaxAge()); + } + return header.toString(); + } + +} \ No newline at end of file diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java index 710e82c99d8..3ae28fc1e05 100644 --- a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java @@ -24,73 +24,31 @@ import static org.wildfly.security.http.oidc.Oidc.OIDC_NAME; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.HashMap; -import java.util.List; import java.util.Map; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.sasl.AuthorizeCallback; - import org.apache.http.HttpStatus; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import org.keycloak.representations.idm.RealmRepresentation; -import org.testcontainers.DockerClientFactory; -import org.wildfly.security.auth.callback.AuthenticationCompleteCallback; -import org.wildfly.security.auth.callback.EvidenceVerifyCallback; -import org.wildfly.security.auth.callback.IdentityCredentialCallback; -import org.wildfly.security.auth.callback.SecurityIdentityCallback; -import org.wildfly.security.auth.server.SecurityDomain; -import org.wildfly.security.evidence.Evidence; import org.wildfly.security.http.HttpServerAuthenticationMechanism; -import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; -import org.wildfly.security.http.HttpServerCookie; -import org.wildfly.security.http.impl.AbstractBaseHttpTest; -import org.wildfly.security.jose.util.JsonSerialization; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; import com.gargoylesoftware.htmlunit.TextPage; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlInput; import com.gargoylesoftware.htmlunit.html.HtmlPage; -import com.gargoylesoftware.htmlunit.javascript.SilentJavaScriptErrorListener; import io.restassured.RestAssured; -import okhttp3.mockwebserver.Dispatcher; -import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.QueueDispatcher; -import okhttp3.mockwebserver.RecordedRequest; /** * Tests for the OpenID Connect authentication mechanism. * * @author Farah Juma */ -public class OidcTest extends AbstractBaseHttpTest { - - public static final String CLIENT_ID = "test-webapp"; - public static final String CLIENT_SECRET = "secret"; - private static KeycloakContainer KEYCLOAK_CONTAINER; - private static final String TEST_REALM = "WildFly"; - private static final String KEYCLOAK_USERNAME = "username"; - private static final String KEYCLOAK_PASSWORD = "password"; - private static final String KEYCLOAK_LOGIN = "login"; - private static final int CLIENT_PORT = 5002; - private static final String CLIENT_APP = "clientApp"; - private static final String CLIENT_PAGE_TEXT = "Welcome page!"; - private static final String CLIENT_HOST_NAME = "localhost"; - private static MockWebServer client; // to simulate the application being secured - - protected HttpServerAuthenticationMechanismFactory oidcFactory; +public class OidcTest extends OidcBaseTest { @BeforeClass public static void startTestContainers() throws Exception { @@ -102,30 +60,6 @@ public static void startTestContainers() throws Exception { client.start(CLIENT_PORT); } - private static Dispatcher createAppResponse(HttpServerAuthenticationMechanism mechanism, int expectedStatusCode, String expectedLocation, String clientPageText) { - return new Dispatcher() { - @Override - public MockResponse dispatch(RecordedRequest recordedRequest) throws InterruptedException { - String path = recordedRequest.getPath(); - if (path.contains("/" + CLIENT_APP) && path.contains("&code=")) { - try { - TestingHttpServerRequest request = new TestingHttpServerRequest(null, - new URI(recordedRequest.getRequestUrl().toString()), recordedRequest.getHeader("Cookie")); - mechanism.evaluateRequest(request); - TestingHttpServerResponse response = request.getResponse(); - assertEquals(expectedStatusCode, response.getStatusCode()); - assertEquals(expectedLocation, response.getLocation()); - return new MockResponse().setBody(clientPageText); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - return new MockResponse() - .setBody(""); - } - }; - } - @AfterClass public static void generalCleanup() throws Exception { if (KEYCLOAK_CONTAINER != null) { @@ -141,21 +75,6 @@ public static void generalCleanup() throws Exception { } } - private static void sendRealmCreationRequest(RealmRepresentation realm) { - try { - RestAssured - .given() - .auth().oauth2(KeycloakConfiguration.getAdminAccessToken(KEYCLOAK_CONTAINER.getAuthServerUrl())) - .contentType("application/json") - .body(JsonSerialization.writeValueAsBytes(realm)) - .when() - .post(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/admin/realms").then() - .statusCode(201); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - @Test public void testWrongPassword() throws Exception { Map props = new HashMap<>(); @@ -240,27 +159,6 @@ private void performAuthentication(InputStream oidcConfig, String username, Stri } } - private WebClient getWebClient() { - WebClient webClient = new WebClient(); - webClient.setCssErrorHandler(new SilentCssErrorHandler()); - webClient.setJavaScriptErrorListener(new SilentJavaScriptErrorListener()); - return webClient; - } - - private HtmlInput loginToKeycloak(String username, String password, URI requestUri, String location, List cookies) throws IOException { - WebClient webClient = getWebClient(); - if (cookies != null) { - for (HttpServerCookie cookie : cookies) { - webClient.addCookie(getCookieString(cookie), requestUri.toURL(), null); - } - } - HtmlPage keycloakLoginPage = webClient.getPage(location); - HtmlForm loginForm = keycloakLoginPage.getForms().get(0); - loginForm.getInputByName(KEYCLOAK_USERNAME).setValueAttribute(username); - loginForm.getInputByName(KEYCLOAK_PASSWORD).setValueAttribute(password); - return loginForm.getInputByName(KEYCLOAK_LOGIN); - } - private InputStream getOidcConfigurationInputStream() { return getOidcConfigurationInputStream(CLIENT_SECRET); } @@ -321,65 +219,4 @@ private InputStream getOidcConfigurationInputStreamWithTokenSignatureAlgorithm() "}"; return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); } - - private CallbackHandler getCallbackHandler() { - return callbacks -> { - for(Callback callback : callbacks) { - if (callback instanceof EvidenceVerifyCallback) { - Evidence evidence = ((EvidenceVerifyCallback) callback).getEvidence(); - ((EvidenceVerifyCallback) callback).setVerified(evidence.getDecodedPrincipal() != null); - } else if (callback instanceof AuthenticationCompleteCallback) { - // NO-OP - } else if (callback instanceof IdentityCredentialCallback) { - // NO-OP - } else if (callback instanceof AuthorizeCallback) { - ((AuthorizeCallback) callback).setAuthorized(true); - } else if (callback instanceof SecurityIdentityCallback) { - ((SecurityIdentityCallback) callback).setSecurityIdentity(SecurityDomain.builder().build().getCurrentSecurityIdentity()); - } else { - throw new UnsupportedCallbackException(callback); - } - } - }; - } - - private static boolean isDockerAvailable() { - try { - DockerClientFactory.instance().client(); - return true; - } catch (Throwable ex) { - return false; - } - } - - private String getCookieString(HttpServerCookie cookie) { - final StringBuilder header = new StringBuilder(cookie.getName()); - header.append("="); - if(cookie.getValue() != null) { - header.append(cookie.getValue()); - } - if (cookie.getPath() != null) { - header.append("; Path="); - header.append(cookie.getPath()); - } - if (cookie.getDomain() != null) { - header.append("; Domain="); - header.append(cookie.getDomain()); - } - if (cookie.isSecure()) { - header.append("; Secure"); - } - if (cookie.isHttpOnly()) { - header.append("; HttpOnly"); - } - if (cookie.getMaxAge() >= 0) { - header.append("; Max-Age="); - header.append(cookie.getMaxAge()); - } - return header.toString(); - } - - private static String getClientUrl() { - return "http://" + CLIENT_HOST_NAME + ":" + CLIENT_PORT + "/" + CLIENT_APP; - } } \ No newline at end of file diff --git a/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java b/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java index ecbfdc47aee..8cebcd38394 100644 --- a/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java +++ b/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java @@ -35,6 +35,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -123,35 +124,52 @@ protected enum Status { protected static class TestingHttpServerRequest implements HttpServerRequest { - private String[] authorization; private Status result; private HttpServerMechanismsResponder responder; private String remoteUser; private URI requestURI; private List cookies; + private String requestMethod = "GET"; + private Map> requestHeaders = new HashMap<>(); public TestingHttpServerRequest(String[] authorization) { - this.authorization = authorization; + if (authorization != null) { + requestHeaders.put(AUTHORIZATION, Arrays.asList(authorization)); + } this.remoteUser = null; this.cookies = new ArrayList<>(); } public TestingHttpServerRequest(String[] authorization, URI requestURI) { - this.authorization = authorization; + if (authorization != null) { + requestHeaders.put(AUTHORIZATION, Arrays.asList(authorization)); + } this.remoteUser = null; this.requestURI = requestURI; this.cookies = new ArrayList<>(); } public TestingHttpServerRequest(String[] authorization, URI requestURI, List cookies) { - this.authorization = authorization; + if (authorization != null) { + requestHeaders.put(AUTHORIZATION, Arrays.asList(authorization)); + } this.remoteUser = null; this.requestURI = requestURI; this.cookies = cookies; } + public TestingHttpServerRequest(Map> requestHeaders, URI requestURI, String requestMethod) { + this.requestHeaders = requestHeaders; + this.remoteUser = null; + this.requestURI = requestURI; + this.cookies = new ArrayList<>(); + this.requestMethod = requestMethod; + } + public TestingHttpServerRequest(String[] authorization, URI requestURI, String cookie) { - this.authorization = authorization; + if (authorization != null) { + requestHeaders.put(AUTHORIZATION, Arrays.asList(authorization)); + } this.remoteUser = null; this.requestURI = requestURI; this.cookies = new ArrayList<>(); @@ -214,14 +232,12 @@ public TestingHttpServerResponse getResponse() throws HttpAuthenticationExceptio } public List getRequestHeaderValues(String headerName) { - if (AUTHORIZATION.equals(headerName)) { - return authorization == null ? null : Arrays.asList(authorization); - } - return null; + return requestHeaders.get(headerName); } public String getFirstRequestHeaderValue(String headerName) { - throw new IllegalStateException(); + List headerValues = requestHeaders.get(headerName); + return headerValues != null ? headerValues.get(0) : null; } public SSLSession getSSLSession() { @@ -262,7 +278,7 @@ public void badRequest(HttpAuthenticationException failure, HttpServerMechanisms } public String getRequestMethod() { - return "GET"; + return requestMethod; } public URI getRequestURI() { @@ -366,9 +382,8 @@ public String getRemoteUser() { protected static class TestingHttpServerResponse implements HttpServerResponse { private int statusCode; - private String authenticate; - private String location; private List cookies; + private Map> responseHeaders = new HashMap<>(); public void setStatusCode(int statusCode) { this.statusCode = statusCode; @@ -379,19 +394,22 @@ public int getStatusCode() { } public void addResponseHeader(String headerName, String headerValue) { - if (WWW_AUTHENTICATE.equals(headerName)) { - authenticate = headerValue; - } else if (LOCATION.equals(headerName)) { - location = headerValue; + if (headerValue != null) { + responseHeaders.put(headerName, Collections.singletonList(headerValue)); } } public String getAuthenticateHeader() { - return authenticate; + return getFirstResponseHeaderValue(WWW_AUTHENTICATE); } public String getLocation() { - return location; + return getFirstResponseHeaderValue(LOCATION); + } + + public String getFirstResponseHeaderValue(String headerName) { + List headerValue = responseHeaders.get(headerName); + return headerValue == null ? null : headerValue.get(0); } public List getCookies() { @@ -472,11 +490,12 @@ protected CallbackHandler getCallbackHandler(String username, String realm, Stri public class TestingHttpExchangeSpi implements HttpExchangeSpi { - private List requestAuthorizationHeaders = Collections.emptyList(); + private Map> requestHeaders = new HashMap<>(); private List responseAuthenticateHeaders = new LinkedList<>(); private List responseAuthenticationInfoHeaders = new LinkedList<>(); private int statusCode; private Status result; + private String requestMethod = "GET"; public int getStatusCode() { return statusCode; @@ -495,17 +514,27 @@ public List getResponseAuthenticationInfoHeaders() { } public void setRequestAuthorizationHeaders(List requestAuthorizationHeaders) { - this.requestAuthorizationHeaders = requestAuthorizationHeaders; + requestHeaders.put(AUTHORIZATION, requestAuthorizationHeaders); + } + + public void setHeader(String headerName, String headerValue) { + if (headerValue != null) { + setHeader(headerName, Collections.singletonList(headerValue)); + } + } + + public void setHeader(String headerName, List headerValue) { + requestHeaders.put(headerName, headerValue); + } + + public void setRequestMethod(String requestMethod) { + this.requestMethod = requestMethod; } // ------ public List getRequestHeaderValues(String headerName) { - if (AUTHORIZATION.equals(headerName)) { - return requestAuthorizationHeaders; - } else { - throw new IllegalStateException(); - } + return requestHeaders.get(headerName); } public void addResponseHeader(String headerName, String headerValue) { @@ -535,7 +564,7 @@ public void badRequest(HttpAuthenticationException error, String mechanismName) } public String getRequestMethod() { - return "GET"; + return requestMethod; } public URI getRequestURI() { From d4f222faa04c5bfabc220b9c42717d4e68c78dc3 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Tue, 26 Jul 2022 18:05:33 -0400 Subject: [PATCH 08/12] [ELY-2362] Small fixes for bearer-only support --- .../java/org/wildfly/security/http/HttpConstants.java | 3 +++ .../security/http/oidc/BasicAuthRequestAuthenticator.java | 2 +- .../http/oidc/BearerTokenRequestAuthenticator.java | 2 +- .../oidc/QueryParameterTokenRequestAuthenticator.java | 2 +- .../wildfly/security/http/oidc/RequestAuthenticator.java | 4 ++-- .../org/wildfly/security/http/oidc/ServerRequest.java | 8 +------- 6 files changed, 9 insertions(+), 12 deletions(-) diff --git a/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java b/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java index 1ef8cefec1a..cb3d074a74d 100644 --- a/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java +++ b/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java @@ -207,6 +207,9 @@ private HttpConstants() { /** * Bearer token pattern. + * The Bearer token authorization header is of the form "Bearer", followed by optional whitespace, followed by + * the token itself, followed by optional whitespace. The token itself must be one or more characters and must + * not contain any whitespace. */ public static final Pattern BEARER_TOKEN_PATTERN = Pattern.compile("^Bearer *([^ ]+) *$", Pattern.CASE_INSENSITIVE); diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/BasicAuthRequestAuthenticator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/BasicAuthRequestAuthenticator.java index 03376c83a70..0e3ea0f19f0 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/BasicAuthRequestAuthenticator.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/BasicAuthRequestAuthenticator.java @@ -34,7 +34,7 @@ * @author Bill Burke * @author Farah Juma */ -public class BasicAuthRequestAuthenticator extends BearerTokenRequestAuthenticator { +class BasicAuthRequestAuthenticator extends BearerTokenRequestAuthenticator { private static final String CHALLENGE_PREFIX = "Basic "; diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/BearerTokenRequestAuthenticator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/BearerTokenRequestAuthenticator.java index b14f363efa3..d732f82c28e 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/BearerTokenRequestAuthenticator.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/BearerTokenRequestAuthenticator.java @@ -40,7 +40,7 @@ * @author Bill Burke * @author Farah Juma */ -public class BearerTokenRequestAuthenticator { +class BearerTokenRequestAuthenticator { protected OidcHttpFacade facade; protected OidcClientConfiguration oidcClientConfiguration; protected AuthChallenge challenge; diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/QueryParameterTokenRequestAuthenticator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/QueryParameterTokenRequestAuthenticator.java index 7d973144862..d72b2f42f4b 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/QueryParameterTokenRequestAuthenticator.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/QueryParameterTokenRequestAuthenticator.java @@ -24,7 +24,7 @@ * @author John D. Ament * @author Farah Juma */ -public class QueryParameterTokenRequestAuthenticator extends BearerTokenRequestAuthenticator { +class QueryParameterTokenRequestAuthenticator extends BearerTokenRequestAuthenticator { public static final String ACCESS_TOKEN = "access_token"; public QueryParameterTokenRequestAuthenticator(OidcHttpFacade facade, OidcClientConfiguration oidcClientConfiguration) { diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java index 438f83b6be1..81c2c9b784f 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java @@ -152,7 +152,7 @@ private AuthOutcome doAuthenticate() { log.debug("NOT_ATTEMPTED: bearer only"); return AuthOutcome.NOT_ATTEMPTED; } - if (isAutodetectedBearerOnly(facade.getRequest())) { + if (isAutodetectedBearerOnly()) { challenge = bearer.getChallenge(); log.debug("NOT_ATTEMPTED: Treating as bearer only"); return AuthOutcome.NOT_ATTEMPTED; @@ -214,7 +214,7 @@ protected void completeAuthentication(BearerTokenRequestAuthenticator bearer) { log.debugv("User ''{0}'' invoking ''{1}'' on client ''{2}''", principal.getName(), facade.getRequest().getURI(), deployment.getResourceName()); } - protected boolean isAutodetectedBearerOnly(OidcHttpFacade.Request request) { + protected boolean isAutodetectedBearerOnly() { if (! deployment.isAutodetectBearerOnly()) return false; String headerValue = facade.getRequest().getHeader(X_REQUESTED_WITH); diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java index d938cec0a29..a39554f901c 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java @@ -269,14 +269,8 @@ public static AccessAndIDTokenResponse getBearerToken(OidcClientConfiguration oi if (entity == null) { throw log.noMessageEntity(); } - InputStream is = entity.getContent(); - try { + try (InputStream is = entity.getContent()) { tokenResponse = JsonSerialization.readValue(is, AccessAndIDTokenResponse.class); - } finally { - try { - is.close(); - } catch (java.io.IOException ignored) { - } } return tokenResponse; } From b32780cda8c7c39c77ed13b33ab69297000ce2c0 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Mon, 8 Aug 2022 18:26:24 -0400 Subject: [PATCH 09/12] [ELY-2356] No need to sanitize the providerUrl --- .../oidc/OidcClientConfigurationBuilder.java | 10 +--------- .../wildfly/security/http/oidc/OidcTest.java | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java index 9f0a319d7b6..99f9b185a5d 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java @@ -19,7 +19,6 @@ package org.wildfly.security.http.oidc; import static org.wildfly.security.http.oidc.ElytronMessages.log; -import static org.wildfly.security.http.oidc.Oidc.SLASH; import static org.wildfly.security.http.oidc.Oidc.SSLRequired; import static org.wildfly.security.http.oidc.Oidc.TokenStore; @@ -147,7 +146,7 @@ protected OidcClientConfiguration internalBuild(final OidcJsonConfiguration oidc } oidcClientConfiguration.setClient(createHttpClientProducer(oidcJsonConfiguration)); oidcClientConfiguration.setAuthServerBaseUrl(oidcJsonConfiguration); - oidcClientConfiguration.setProviderUrl(sanitizeProviderUrl(oidcJsonConfiguration.getProviderUrl())); + oidcClientConfiguration.setProviderUrl(oidcJsonConfiguration.getProviderUrl()); if (oidcJsonConfiguration.getTurnOffChangeSessionIdOnLogin() != null) { oidcClientConfiguration.setTurnOffChangeSessionIdOnLogin(oidcJsonConfiguration.getTurnOffChangeSessionIdOnLogin()); } @@ -157,13 +156,6 @@ protected OidcClientConfiguration internalBuild(final OidcJsonConfiguration oidc return oidcClientConfiguration; } - private static String sanitizeProviderUrl(String providerUrl) { - if (providerUrl != null && providerUrl.endsWith(SLASH)) { - return providerUrl.substring(0, providerUrl.length() - 1); - } - return providerUrl; - } - private Callable createHttpClientProducer(final OidcJsonConfiguration oidcJsonConfiguration) { return new Callable() { private HttpClient client; diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java index 3ae28fc1e05..3df0fce7410 100644 --- a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java @@ -123,6 +123,12 @@ public void testSucessfulAuthenticationWithProviderUrl() throws Exception { true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); } + @Test + public void testSucessfulAuthenticationWithProviderUrlTrailingSlash() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithProviderUrlTrailingSlash(), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + @Test public void testTokenSignatureAlgorithm() throws Exception { // keycloak uses RS256 @@ -194,6 +200,19 @@ private InputStream getOidcConfigurationInputStreamWithProviderUrl() { return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); } + private InputStream getOidcConfigurationInputStreamWithProviderUrlTrailingSlash() { + String oidcConfig = "{\n" + + " \"resource\" : \"" + CLIENT_ID + "\",\n" + + " \"public-client\" : \"false\",\n" + + " \"provider-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM + "/" + "\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"credentials\" : {\n" + + " \"secret\" : \"" + CLIENT_SECRET + "\"\n" + + " }\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } + private InputStream getOidcConfigurationMissingRequiredOption() { String oidcConfig = "{\n" + " \"public-client\" : \"false\",\n" + From 39d9716ce1e7653d8e60dda4b871c7c22c80d6e0 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Mon, 8 Aug 2022 17:49:27 -0400 Subject: [PATCH 10/12] [ELY-2357] Set the issuer URL for OIDC based on discovery --- .../security/http/oidc/OidcClientConfiguration.java | 12 ++++++++---- .../wildfly/security/http/oidc/TokenValidator.java | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java index 79e10a337f6..db872b30a89 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java @@ -79,6 +79,7 @@ public enum RelativeUrlsUsed { protected String registerNodeUrl; protected String unregisterNodeUrl; protected String jwksUrl; + protected String issuerUrl; protected String principalAttribute = "sub"; protected String resource; @@ -218,15 +219,13 @@ protected void resolveUrls() { OidcProviderMetadata config = getOidcProviderMetadata(discoveryUrl); authUrl = config.getAuthorizationEndpoint(); - if (providerUrl == null) { - providerUrl = config.getIssuer(); - } + issuerUrl = config.getIssuer(); tokenUrl = config.getTokenEndpoint(); logoutUrl = config.getLogoutEndpoint(); jwksUrl = config.getJwksUri(); if (authServerBaseUrl != null) { // keycloak-specific properties - accountUrl = getUrl(providerUrl, ACCOUNT_PATH); + accountUrl = getUrl(issuerUrl, ACCOUNT_PATH); registerNodeUrl = getUrl(authServerBaseUrl, KEYCLOAK_REALMS_PATH + getRealm(), CLIENTS_MANAGEMENT_REGISTER_NODE_PATH); unregisterNodeUrl = getUrl(authServerBaseUrl, KEYCLOAK_REALMS_PATH + getRealm(), CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH); } @@ -325,6 +324,11 @@ public String getJwksUrl() { return jwksUrl; } + public String getIssuerUrl() { + resolveUrls(); + return issuerUrl; + } + public void setResource(String resource) { this.resource = resource; } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java index a6f5bde57bc..a549331b842 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java @@ -172,7 +172,7 @@ public static class Builder { * @throws IllegalArgumentException if a required builder parameter is missing or invalid */ public TokenValidator build() throws IllegalArgumentException { - expectedIssuer = clientConfiguration.getProviderUrl(); + expectedIssuer = clientConfiguration.getIssuerUrl(); if (expectedIssuer == null || expectedIssuer.length() == 0) { throw log.noExpectedIssuerGiven(); } From 8931eed589efe0923d7f8bc6acbf9dcaa24eaa7d Mon Sep 17 00:00:00 2001 From: Santos Zatarain Date: Fri, 28 Oct 2022 23:07:54 -0500 Subject: [PATCH 11/12] [ELY-2487] Misplaced invocation of isAutodetectedBearerOnly() Method isAutodetectedBearerOnly() should be invoked after checking cached token. Invoking isAutodetectedBearerOnly() early will break every AJAX request that relies on HTTP session. A clear example is JSF Partial Request, it will never send the header "Authorization" neither the query parameter "auth". During the initial load of view the user was authenticated, then the token was stored in HTTP session, so, JSF Partial Request relies on HTTP session onwards. https://issues.redhat.com/browse/ELY-2487 --- .../security/http/oidc/RequestAuthenticator.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java index 81c2c9b784f..a8084523a20 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java @@ -152,11 +152,6 @@ private AuthOutcome doAuthenticate() { log.debug("NOT_ATTEMPTED: bearer only"); return AuthOutcome.NOT_ATTEMPTED; } - if (isAutodetectedBearerOnly()) { - challenge = bearer.getChallenge(); - log.debug("NOT_ATTEMPTED: Treating as bearer only"); - return AuthOutcome.NOT_ATTEMPTED; - } if (log.isTraceEnabled()) { log.trace("try oidc"); @@ -168,6 +163,12 @@ private AuthOutcome doAuthenticate() { return AuthOutcome.AUTHENTICATED; } + if (isAutodetectedBearerOnly()) { + challenge = bearer.getChallenge(); + log.debug("NOT_ATTEMPTED: Treating as bearer only"); + return AuthOutcome.NOT_ATTEMPTED; + } + OidcRequestAuthenticator oidc = createOidcAuthenticator(); outcome = oidc.authenticate(); if (outcome == AuthOutcome.FAILED) { From d541f5175e6509860ca19eec600f5ee87edcad2c Mon Sep 17 00:00:00 2001 From: Patrick Reinhart Date: Wed, 2 Feb 2022 20:13:23 +0100 Subject: [PATCH 12/12] [ELY-2303] enables combined realm & resource roles Signed-off-by: Patrick Reinhart --- .../security/http/oidc/OidcSecurityRealm.java | 23 +- .../http/oidc/OidcSecurityRealmTest.java | 213 ++++++++++++++++++ 2 files changed, 223 insertions(+), 13 deletions(-) create mode 100644 http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcSecurityRealmTest.java diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcSecurityRealm.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcSecurityRealm.java index 9307b634912..f66b7e1c123 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcSecurityRealm.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcSecurityRealm.java @@ -22,9 +22,8 @@ import java.security.Principal; import java.security.spec.AlgorithmParameterSpec; -import java.util.Collections; +import java.util.HashSet; import java.util.Set; -import java.util.stream.Collectors; import org.wildfly.security.auth.SupportLevel; import org.wildfly.security.auth.server.RealmIdentity; @@ -101,29 +100,27 @@ public SupportLevel getEvidenceVerifySupport(Class evidenceT } private static Set getRolesFromSecurityContext(RefreshableOidcSecurityContext session) { - Set roles = null; - AccessToken accessToken = session.getToken(); - if (session.getOidcClientConfiguration().isUseResourceRoleMappings()) { + final Set roles = new HashSet<>(); + final AccessToken accessToken = session.getToken(); + final OidcClientConfiguration oidcClientConfig = session.getOidcClientConfiguration(); + if (oidcClientConfig.isUseResourceRoleMappings()) { if (log.isTraceEnabled()) { - log.trace("useResourceRoleMappings"); + log.trace("use resource role mappings"); } - RealmAccessClaim resourceAccessClaim = accessToken.getResourceAccessClaim(session.getOidcClientConfiguration().getResourceName()); + RealmAccessClaim resourceAccessClaim = accessToken.getResourceAccessClaim(oidcClientConfig.getResourceName()); if (resourceAccessClaim != null) { - roles = resourceAccessClaim.getRoles().stream().collect(Collectors.toSet()); + roles.addAll(resourceAccessClaim.getRoles()); } } - if (session.getOidcClientConfiguration().isUseRealmRoleMappings()) { + if (oidcClientConfig.isUseRealmRoleMappings()) { if (log.isTraceEnabled()) { log.trace("use realm role mappings"); } RealmAccessClaim realmAccessClaim = accessToken.getRealmAccessClaim(); if (realmAccessClaim != null) { - roles = realmAccessClaim.getRoles().stream().collect(Collectors.toSet()); + roles.addAll(realmAccessClaim.getRoles()); } } - if (roles == null) { - roles = Collections.emptySet(); - } if (log.isTraceEnabled()) { log.trace("Setting roles: "); for (String role : roles) { diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcSecurityRealmTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcSecurityRealmTest.java new file mode 100644 index 00000000000..7ef6900be23 --- /dev/null +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcSecurityRealmTest.java @@ -0,0 +1,213 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2021 Red Hat, Inc., and individual 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.wildfly.security.http.oidc; + +import org.jboss.resteasy.plugins.server.embedded.SimplePrincipal; +import org.jose4j.jwt.JwtClaims; +import org.junit.Before; +import org.junit.Test; +import org.wildfly.security.auth.SupportLevel; +import org.wildfly.security.auth.server.RealmIdentity; +import org.wildfly.security.auth.server.RealmUnavailableException; +import org.wildfly.security.authz.Attributes; +import org.wildfly.security.authz.AuthorizationIdentity; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.wildfly.security.auth.SupportLevel.POSSIBLY_SUPPORTED; +import static org.wildfly.security.auth.SupportLevel.UNSUPPORTED; +import static org.wildfly.security.auth.server.RealmIdentity.NON_EXISTENT; +import static org.wildfly.security.authz.RoleDecoder.KEY_ROLES; + +/** + * Tests the {@link OidcSecurityRealm} implementation. + * + * @author Patrick Reinhart + */ +public class OidcSecurityRealmTest { + private OidcSecurityRealm realm; + + @Before + public void setUp() { + realm = new OidcSecurityRealm(); + } + + @Test + public void testGetCredentialAcquireSupport() throws RealmUnavailableException { + assertEquals(UNSUPPORTED, realm.getCredentialAcquireSupport(null, null, null)); + } + + @Test + public void testGetEvidenceVerifySupport() throws RealmUnavailableException { + assertEquals(POSSIBLY_SUPPORTED, realm.getEvidenceVerifySupport(null, null)); + } + + @Test + public void testGetRealmIdentityWithNonOidcPrincipal() throws RealmUnavailableException { + Principal nonOidcPricipal = new SimplePrincipal("john"); + assertEquals(NON_EXISTENT, realm.getRealmIdentity(nonOidcPricipal)); + } + + @Test + public void testGetRealmIdentityNoRoles() throws RealmUnavailableException { + // setup + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(); + securityContext.setCurrentRequestInfo(new OidcClientConfiguration(), null); + OidcPrincipal principal = new OidcPrincipal("john", securityContext); + + // test + RealmIdentity identity = realm.getRealmIdentity(principal); + + // verification + assertNotEquals(NON_EXISTENT, identity); + assertEquals(principal, identity.getRealmIdentityPrincipal()); + assertEquals(UNSUPPORTED, identity.getCredentialAcquireSupport(null, null, null)); + assertNull(identity.getCredential(null)); + assertEquals(SupportLevel.SUPPORTED, identity.getEvidenceVerifySupport(null, null)); + assertTrue(identity.verifyEvidence(null)); + assertTrue(identity.exists()); + AuthorizationIdentity authorizationIdentity = identity.getAuthorizationIdentity(); + assertNotNull(authorizationIdentity); + final Attributes.Entry roles = authorizationIdentity.getAttributes().get(KEY_ROLES); + assertTrue(roles.isEmpty()); + } + + @Test + public void testGetRealmIdentityRolesCombined() throws RealmUnavailableException { + // setup + final OidcClientConfiguration clientConfiguration = new OidcClientConfiguration(); + clientConfiguration.setClientId("SpecialResource"); + clientConfiguration.setUseRealmRoleMappings(true); + clientConfiguration.setUseResourceRoleMappings(true); + final JwtClaims jwtClaims = new JwtClaims(); + final Map resourceAccess = new HashMap<>(); + resourceAccess.put("SomeResource", createRoles("roleA")); + resourceAccess.put("SpecialResource", createRoles("roleB", "roleC")); + jwtClaims.setClaim("resource_access", resourceAccess); + jwtClaims.setClaim("realm_access", createRoles("roleC", "roleD")); + + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration, + null, null, new AccessToken(jwtClaims), null, null, null); + OidcPrincipal principal = new OidcPrincipal("john", securityContext); + + // test + RealmIdentity identity = realm.getRealmIdentity(principal); + AuthorizationIdentity authorizationIdentity = identity.getAuthorizationIdentity(); + final Attributes.Entry roles = authorizationIdentity.getAttributes().get(KEY_ROLES); + + // verification + assertEquals(3, roles.size()); + assertTrue(roles.contains("roleB")); + assertTrue(roles.contains("roleC")); + assertTrue(roles.contains("roleD")); + } + + @Test + public void testGetRealmIdentityOnlyRealmRoles() throws RealmUnavailableException { + // setup + final OidcClientConfiguration clientConfiguration = new OidcClientConfiguration(); + clientConfiguration.setClientId("SpecialResource"); + clientConfiguration.setUseRealmRoleMappings(true); + final JwtClaims jwtClaims = new JwtClaims(); + final Map resourceAccess = new HashMap<>(); + resourceAccess.put("SpecialResource", createRoles("roleB", "roleC")); + jwtClaims.setClaim("resource_access", resourceAccess); + jwtClaims.setClaim("realm_access", createRoles("roleC", "roleD")); + + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration, + null, null, new AccessToken(jwtClaims), null, null, null); + OidcPrincipal principal = new OidcPrincipal("john", securityContext); + + // test + AuthorizationIdentity authorizationIdentity = realm.getRealmIdentity(principal).getAuthorizationIdentity(); + final Attributes.Entry roles = authorizationIdentity.getAttributes().get(KEY_ROLES); + + // verification + assertEquals(2, roles.size()); + assertTrue(roles.contains("roleC")); + assertTrue(roles.contains("roleD")); + } + + @Test + public void testGetRealmIdentityOnlyResourceRoles() throws RealmUnavailableException { + // setup + final OidcClientConfiguration clientConfiguration = new OidcClientConfiguration(); + clientConfiguration.setClientId("SpecialResource"); + clientConfiguration.setUseRealmRoleMappings(false); + clientConfiguration.setUseResourceRoleMappings(true); + final JwtClaims jwtClaims = new JwtClaims(); + final Map resourceAccess = new HashMap<>(); + resourceAccess.put("SpecialResource", createRoles("roleB", "roleC")); + jwtClaims.setClaim("resource_access", resourceAccess); + jwtClaims.setClaim("", new RealmAccessClaim(createRoles("roleC", "roleD"))); + + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration, + null, null, new AccessToken(jwtClaims), null, null, null); + OidcPrincipal principal = new OidcPrincipal("john", securityContext); + + // test + AuthorizationIdentity authorizationIdentity = realm.getRealmIdentity(principal).getAuthorizationIdentity(); + final Attributes.Entry roles = authorizationIdentity.getAttributes().get(KEY_ROLES); + + // verification + assertEquals(2, roles.size()); + assertTrue(roles.contains("roleB")); + assertTrue(roles.contains("roleC")); + } + + + @Test + public void testGetRealmIdentityNoMappings() throws RealmUnavailableException { + // setup + final OidcClientConfiguration clientConfiguration = new OidcClientConfiguration(); + clientConfiguration.setClientId("SpecialResource"); + clientConfiguration.setUseRealmRoleMappings(false); + clientConfiguration.setUseResourceRoleMappings(false); + final JwtClaims jwtClaims = new JwtClaims(); + final Map resourceAccess = new HashMap<>(); + resourceAccess.put("SpecialResource", createRoles("roleB", "roleC")); + jwtClaims.setClaim("resource_access", resourceAccess); + jwtClaims.setClaim("", new RealmAccessClaim(createRoles("roleC", "roleD"))); + + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration, + null, null, new AccessToken(jwtClaims), null, null, null); + OidcPrincipal principal = new OidcPrincipal("john", securityContext); + + // test + AuthorizationIdentity authorizationIdentity = realm.getRealmIdentity(principal).getAuthorizationIdentity(); + final Attributes.Entry roles = authorizationIdentity.getAttributes().get(KEY_ROLES); + + // verification + assertTrue(roles.isEmpty()); + } + + static Map createRoles(String... roleNames) { + final ArrayList value = new ArrayList<>(); + for (String role : roleNames) { + value.add(role); + } + final Map roles = new HashMap<>(); + roles.put("roles", value); + return roles; + } +}