From 84efd01ce5b04f52a6e8d4a335a43e0f87c3b9ea Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Thu, 23 Jun 2022 17:18:04 -0400 Subject: [PATCH 1/8] [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 fe465d730230dc1f6f13f0b03ec0f1bcc9069d8d Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Tue, 28 Jun 2022 18:13:28 -0400 Subject: [PATCH 2/8] [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 a4c5f64fbf4..dba0abeb14d 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); @@ -98,6 +88,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. * @@ -128,9 +167,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 { @@ -157,10 +196,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); @@ -223,6 +260,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 5d2ddd57351b2c7b918f812ed4fe8ad7f1a4efe5 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Thu, 7 Jul 2022 17:48:38 -0400 Subject: [PATCH 3/8] [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 0e94a431032300e55d44d78df032fe90a0c65893 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Fri, 8 Jul 2022 11:01:38 -0400 Subject: [PATCH 4/8] [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 2be0ca424ec4d364a62f37974324e083e9f07c1e Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Mon, 25 Jul 2022 14:49:49 -0400 Subject: [PATCH 5/8] [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 d1817ba69ea360b472430682847a4cf2557b3034 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Mon, 11 Jul 2022 16:08:29 -0400 Subject: [PATCH 6/8] [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 8def1a1011628b24a99f2fe69838d7ebe01bfc91 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Mon, 25 Jul 2022 14:50:04 -0400 Subject: [PATCH 7/8] [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 a86f86d1443..73171b4fb48 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 @@ -36,6 +36,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; @@ -124,35 +125,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<>(); @@ -215,14 +233,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() { @@ -263,7 +279,7 @@ public void badRequest(HttpAuthenticationException failure, HttpServerMechanisms } public String getRequestMethod() { - return "GET"; + return requestMethod; } public URI getRequestURI() { @@ -367,9 +383,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; @@ -380,19 +395,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() { @@ -473,11 +491,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; @@ -496,17 +515,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) { @@ -536,7 +565,7 @@ public void badRequest(HttpAuthenticationException error, String mechanismName) } public String getRequestMethod() { - return "GET"; + return requestMethod; } public URI getRequestURI() { From d950ffa36f3b029607f76275ec00fb8cd037adec Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Tue, 26 Jul 2022 18:05:33 -0400 Subject: [PATCH 8/8] [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; }