From d27ba44439e702964cb668886ccbc35f740b38e8 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 12 Jun 2016 23:03:47 -0700 Subject: [PATCH] GUACAMOLE-210: Validate the JWT using jose.4.j. --- extensions/guacamole-auth-openid/pom.xml | 7 ++ .../oauth/AuthenticationProviderService.java | 13 ++- .../OAuthAuthenticationProviderModule.java | 2 + .../auth/oauth/conf/ConfigurationService.java | 52 ++++++++- .../oauth/conf/OAuthGuacamoleProperties.java | 35 ++++++ .../oauth/token/TokenValidationService.java | 102 ++++++++++++++++++ 6 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/token/TokenValidationService.java diff --git a/extensions/guacamole-auth-openid/pom.xml b/extensions/guacamole-auth-openid/pom.xml index 60691e2d1..fa819c80b 100644 --- a/extensions/guacamole-auth-openid/pom.xml +++ b/extensions/guacamole-auth-openid/pom.xml @@ -86,6 +86,13 @@ provided + + + org.bitbucket.b_c + jose4j + 0.5.1 + + com.google.inject diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/AuthenticationProviderService.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/AuthenticationProviderService.java index 0aac96849..d89f08797 100644 --- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/AuthenticationProviderService.java @@ -23,9 +23,10 @@ import com.google.inject.Inject; import com.google.inject.Provider; import java.util.Arrays; import javax.servlet.http.HttpServletRequest; -import org.apache.guacamole.auth.oauth.user.AuthenticatedUser; import org.apache.guacamole.auth.oauth.conf.ConfigurationService; import org.apache.guacamole.auth.oauth.form.OAuthTokenField; +import org.apache.guacamole.auth.oauth.token.TokenValidationService; +import org.apache.guacamole.auth.oauth.user.AuthenticatedUser; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.form.Field; import org.apache.guacamole.net.auth.Credentials; @@ -51,6 +52,12 @@ public class AuthenticationProviderService { @Inject private ConfigurationService confService; + /** + * Service for validating received ID tokens. + */ + @Inject + private TokenValidationService tokenService; + /** * Provider for AuthenticatedUser objects. */ @@ -82,12 +89,12 @@ public class AuthenticationProviderService { if (request != null) token = request.getParameter(OAuthTokenField.PARAMETER_NAME); - // TODO: Actually validate received token + // If token provided, validate and produce authenticated user if (token != null) { // Create corresponding authenticated user AuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); - authenticatedUser.init("STUB", credentials); + authenticatedUser.init(tokenService.processUsername(token), credentials); return authenticatedUser; } diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/OAuthAuthenticationProviderModule.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/OAuthAuthenticationProviderModule.java index 202e6a267..f83806319 100644 --- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/OAuthAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/OAuthAuthenticationProviderModule.java @@ -21,6 +21,7 @@ package org.apache.guacamole.auth.oauth; import com.google.inject.AbstractModule; import org.apache.guacamole.auth.oauth.conf.ConfigurationService; +import org.apache.guacamole.auth.oauth.token.TokenValidationService; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.environment.LocalEnvironment; @@ -73,6 +74,7 @@ public class OAuthAuthenticationProviderModule extends AbstractModule { // Bind OAuth-specific services bind(ConfigurationService.class); + bind(TokenValidationService.class); } diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/conf/ConfigurationService.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/conf/ConfigurationService.java index 9debab77b..1304d5898 100644 --- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/conf/ConfigurationService.java @@ -79,11 +79,61 @@ public class ConfigurationService { * as configured with guacamole.properties. * * @throws GuacamoleException - * If guacamole.properties cannot be parsed, or if the client secret + * If guacamole.properties cannot be parsed, or if the redirect URI * property is missing. */ public String getRedirectURI() throws GuacamoleException { return environment.getRequiredProperty(OAuthGuacamoleProperties.OAUTH_REDIRECT_URI); } + /** + * Returns the issuer to expect for all received ID tokens, as configured + * with guacamole.properties. + * + * @return + * The issuer to expect for all received ID tokens, as configured with + * guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed, or if the issuer property + * is missing. + */ + public String getIssuer() throws GuacamoleException { + return environment.getRequiredProperty(OAuthGuacamoleProperties.OAUTH_ISSUER); + } + + /** + * Returns the endpoint (URI) of the JWKS service which defines how + * received ID tokens (JWTs) shall be validated, as configured with + * guacamole.properties. + * + * @return + * The endpoint (URI) of the JWKS service which defines how received ID + * tokens (JWTs) shall be validated, as configured with + * guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed, or if the JWKS endpoint + * property is missing. + */ + public String getJWKSEndpoint() throws GuacamoleException { + return environment.getRequiredProperty(OAuthGuacamoleProperties.OAUTH_JWKS_ENDPOINT); + } + + /** + * Returns the claim type which contains the authenticated user's username + * within any valid JWT, as configured with guacamole.properties. + * + * @return + * The claim type which contains the authenticated user's username + * within any valid JWT, as configured with guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed, or if the username claim + * type property is missing. + */ + public String getUsernameClaimType() throws GuacamoleException { + return environment.getRequiredProperty(OAuthGuacamoleProperties.OAUTH_USERNAME_CLAIM_TYPE); + } + } diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/conf/OAuthGuacamoleProperties.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/conf/OAuthGuacamoleProperties.java index 34952fe3a..cfb4eb37a 100644 --- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/conf/OAuthGuacamoleProperties.java +++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/conf/OAuthGuacamoleProperties.java @@ -44,6 +44,41 @@ public class OAuthGuacamoleProperties { }; + /** + * The endpoint (URI) of the JWKS service which defines how received ID + * tokens (JWTs) shall be validated. + */ + public static final StringGuacamoleProperty OAUTH_JWKS_ENDPOINT = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "oauth-jwks-endpoint"; } + + }; + + /** + * The issuer to expect for all received ID tokens. + */ + public static final StringGuacamoleProperty OAUTH_ISSUER = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "oauth-issuer"; } + + }; + + /** + * The claim type which contains the authenticated user's username within + * any valid JWT. + */ + public static final StringGuacamoleProperty OAUTH_USERNAME_CLAIM_TYPE = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "oauth-username-claim-type"; } + + }; + /** * OAuth client ID which should be submitted to the OAuth service when * necessary. This value is typically provided by the OAuth service when diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/token/TokenValidationService.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/token/TokenValidationService.java new file mode 100644 index 000000000..a61f7ceee --- /dev/null +++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/oauth/token/TokenValidationService.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.guacamole.auth.oauth.token; + +import com.google.inject.Inject; +import org.apache.guacamole.auth.oauth.conf.ConfigurationService; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleSecurityException; +import org.apache.guacamole.GuacamoleServerException; +import org.jose4j.jwk.HttpsJwks; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.MalformedClaimException; +import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.jwt.consumer.JwtConsumer; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; +import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver; + +/** + * Service for validating ID tokens forwarded to us by the client, verifying + * that they did indeed come from the OAuth service. + */ +public class TokenValidationService { + + @Inject + private ConfigurationService confService; + + /** + * Validates and parses the given ID token, returning the username contained + * therein, as defined by the username claim type given in + * guacamole.properties. If the username claim type is missing or the ID + * token is invalid, an exception is thrown instead. + * + * @param token + * The ID token to validate and parse. + * + * @return + * The username contained within the given ID token. + * + * @throws GuacamoleException + * If the ID token is not valid, the username claim type is missing, or + * guacamole.properties could not be parsed. + */ + public String processUsername(String token) throws GuacamoleException { + + // Validating the token requires a JWKS key resolver + HttpsJwks jwks = new HttpsJwks(confService.getJWKSEndpoint()); + HttpsJwksVerificationKeyResolver resolver = new HttpsJwksVerificationKeyResolver(jwks); + + // Create JWT consumer for validating received token + JwtConsumer jwtConsumer = new JwtConsumerBuilder() + .setRequireExpirationTime() + .setMaxFutureValidityInMinutes(300) + .setAllowedClockSkewInSeconds(30) + .setRequireSubject() + .setExpectedIssuer(confService.getIssuer()) + .setExpectedAudience(confService.getClientID()) + .setVerificationKeyResolver(resolver) + .build(); + + try { + + // Validate JWT + JwtClaims claims = jwtConsumer.processToClaims(token); + + // Pull username from claims + String username = claims.getStringClaimValue(confService.getUsernameClaimType()); + if (username == null) + throw new GuacamoleSecurityException("Username missing from token"); + + // Username successfully retrieved from the JWT + return username; + + } + + // Rethrow any failures to validate/parse the JWT + catch (InvalidJwtException e) { + throw new GuacamoleSecurityException("Invalid ID token.", e); + } + catch (MalformedClaimException e) { + throw new GuacamoleServerException("Unable to parse JWT claims.", e); + } + + } + +}