Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@
import com.google.inject.Singleton;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.core.UriBuilder;
import org.apache.guacamole.auth.openid.conf.ConfigurationService;
import org.apache.guacamole.auth.openid.OpenIDAuthenticationSessionManager;
import org.apache.guacamole.auth.openid.token.TokenValidationService;
import org.apache.guacamole.auth.openid.util.PKCEUtil;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.sso.NonceService;
import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService;
Expand All @@ -40,26 +43,55 @@
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.apache.guacamole.net.auth.IdentifierGenerator;
import org.jose4j.jwt.JwtClaims;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
* Service that authenticates Guacamole users by processing OpenID tokens.
*/
@Singleton
public class AuthenticationProviderService implements SSOAuthenticationProviderService {

/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class);

/**
* The standard HTTP parameter which will be included within the URL by all
* OpenID services upon successful authentication and redirect.
* OpenID services upon successful implicit flow authentication.
*
*/
public static final String TOKEN_PARAMETER_NAME = "id_token";
public static final String IMPLICIT_TOKEN_PARAMETER_NAME = "id_token";

/**
* The standard HTTP parameter which will be included within the URL by all
* OpenID services upon successful code flow authentication. Used to recover
* the stored user state.
*/
public static final String CODE_TOKEN_PARAMETER_NAME = "code";

/**
* The name of the query parameter that identifies an active authentication
* session (in-progress OpenID authentication attempt).
*/
public static final String AUTH_SESSION_QUERY_PARAM = "state";

/**
* Service for retrieving OpenID configuration information.
*/
@Inject
private ConfigurationService confService;

/**
* Manager of active OpenID authentication attempts.
*/
@Inject
private OpenIDAuthenticationSessionManager sessionManager;

/**
* Service for validating and generating unique nonce values.
*/
Expand All @@ -78,6 +110,25 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS
@Inject
private Provider<SSOAuthenticatedUser> authenticatedUserProvider;

/**
* Return the value of the session identifier associated with the given
* credentials, or null if no session identifier is found in the
* credentials.
*
* @param credentials
* The credentials from which to extract the session identifier.
*
* @return
* The session identifier associated with the given credentials, or
* null if no identifier is found.
*/
public static String getSessionIdentifier(Credentials credentials) {

// Return the session identifier from the request params, if set, or
// null otherwise
return credentials != null ? credentials.getParameter(AUTH_SESSION_QUERY_PARAM) : null;
}

@Override
public SSOAuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
Expand All @@ -86,33 +137,57 @@ public SSOAuthenticatedUser authenticateUser(Credentials credentials)
Set<String> groups = null;
Map<String,String> tokens = Collections.emptyMap();

// Validate OpenID token in request, if present, and derive username
String token = credentials.getParameter(TOKEN_PARAMETER_NAME);
if (token != null) {
JwtClaims claims = tokenService.validateToken(token);
if (claims != null) {
username = tokenService.processUsername(claims);
groups = tokenService.processGroups(claims);
tokens = tokenService.processAttributes(claims);
logger.debug("OpenID authentication with '{}' reponse type (ID: {}, Secret: {}, PKCE: {})",
confService.getResponseType(),
confService.getClientID(),
confService.getClientSecret(),
confService.isPKCERequired());

if (confService.isImplicitFlow()) {
String token = credentials.getParameter(IMPLICIT_TOKEN_PARAMETER_NAME);
if (token != null) {
JwtClaims claims = tokenService.validateTokenOrCode(token, "");
if (claims != null) {
username = tokenService.processUsername(claims);
groups = tokenService.processGroups(claims);
tokens = tokenService.processAttributes(claims);
}
}
}
else {
String verifier = null;
if (confService.isPKCERequired()) {
// Recover session
String identifier = getSessionIdentifier(credentials);
if (identifier != null) {
verifier = sessionManager.getVerifier(identifier);
}
}
String code = credentials.getParameter("code");
if (code != null && (confService.isPKCERequired() == false || verifier != null)) {
JwtClaims claims = tokenService.validateTokenOrCode(code, verifier);
if (claims != null) {
username = tokenService.processUsername(claims);
groups = tokenService.processGroups(claims);
tokens = tokenService.processAttributes(claims);
}
}
}

// If the username was successfully retrieved from the token, produce
// authenticated user
if (username != null) {

// Create corresponding authenticated user
SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
authenticatedUser.init(username, credentials, groups, tokens);
return authenticatedUser;

}

// Request OpenID token (will automatically redirect the user to the
// OpenID authorization page via JavaScript)
throw new GuacamoleInvalidCredentialsException("Invalid login.",
new CredentialsInfo(Arrays.asList(new Field[] {
new RedirectField(TOKEN_PARAMETER_NAME, getLoginURI(),
new RedirectField(AUTH_SESSION_QUERY_PARAM, getLoginURI(),
new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING"))
}))
);
Expand All @@ -121,13 +196,40 @@ public SSOAuthenticatedUser authenticateUser(Credentials credentials)

@Override
public URI getLoginURI() throws GuacamoleException {
return UriBuilder.fromUri(confService.getAuthorizationEndpoint())
UriBuilder builder = UriBuilder.fromUri(confService.getAuthorizationEndpoint())
.queryParam("scope", confService.getScope())
.queryParam("response_type", "id_token")
.queryParam("response_type", confService.getResponseType().toString())
.queryParam("client_id", confService.getClientID())
.queryParam("redirect_uri", confService.getRedirectURI())
.queryParam("nonce", nonceService.generate(confService.getMaxNonceValidity() * 60000L))
.build();
.queryParam("redirect_uri", confService.getRedirectURI());

if (confService.isImplicitFlow()) {
builder.queryParam("nonce", nonceService.generate(confService.getMaxNonceValidity() * 60000L));
}
else {
if (confService.isPKCERequired()) {
String codeVerifier = PKCEUtil.generateCodeVerifier();
String codeChallenge;

try {
codeChallenge = PKCEUtil.generateCodeChallenge(codeVerifier);
}
catch (Exception e) {
throw new GuacamoleException("Unable to compute PKCE challenge", e);
}

// Store verifier for authenticateUser
OpenIDAuthenticationSession session = new OpenIDAuthenticationSession(codeVerifier,
confService.getMaxPKCEVerifierValidity() * 60000L);
String identifier = IdentifierGenerator.generateIdentifier();
sessionManager.defer(session, identifier);

builder.queryParam("code_challenge", codeChallenge)
.queryParam("code_challenge_method", "S256")
.queryParam(AUTH_SESSION_QUERY_PARAM, identifier);
}
}

return builder.build();
}

@Override
Expand Down Expand Up @@ -156,7 +258,7 @@ public URI getLogoutURI(String idToken) throws GuacamoleException {

@Override
public void shutdown() {
// Nothing to clean up
sessionManager.shutdown();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@

import com.google.inject.AbstractModule;
import com.google.inject.Scopes;
import com.google.inject.Singleton;
import org.apache.guacamole.auth.openid.conf.ConfigurationService;
import org.apache.guacamole.auth.openid.conf.OpenIDEnvironment;
import org.apache.guacamole.auth.openid.conf.OpenIDWellKnown;
import org.apache.guacamole.auth.openid.OpenIDAuthenticationSessionManager;
import org.apache.guacamole.auth.sso.NonceService;
import org.apache.guacamole.auth.openid.token.TokenValidationService;
import org.apache.guacamole.environment.Environment;
Expand All @@ -42,7 +45,8 @@ protected void configure() {
bind(ConfigurationService.class);
bind(NonceService.class).in(Scopes.SINGLETON);
bind(TokenValidationService.class);

bind(OpenIDAuthenticationSessionManager.class);

bind(Environment.class).toInstance(environment);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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.openid;

import org.apache.guacamole.net.auth.AuthenticationSession;

/**
* Representation of an in-progress OpenID authentication attempt.
*/

public class OpenIDAuthenticationSession extends AuthenticationSession {
/**
* The PKCE challenge verifier.
*/
private final String verifier;

/**
* Creates a new AuthenticationSession representing an in-progress OpenID
* authentication attempt.
*
* @param expires
* The number of milliseconds that may elapse before this session must
* be considered invalid.
*/
public OpenIDAuthenticationSession(String verifier, long expires) {
super(expires);
this.verifier = verifier;
}

/**
* Returns the stored PKCE verifier
*
* @return
* The PKCE verifier
*/
public String getVerifier() {
return verifier;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.openid;

import com.google.inject.Singleton;
import org.apache.guacamole.net.auth.AuthenticationSessionManager;

/**
* Manager service that temporarily stores OpenID authentication attempts while
* the authentication flow is underway.
*/
@Singleton
public class OpenIDAuthenticationSessionManager
extends AuthenticationSessionManager<OpenIDAuthenticationSession> {

/**
* Returns the stored PKCE verifier used with the identity provider
*
* @param identifier
* The unique string returned by the call to defer(). For convenience,
* this value may safely be null.
*
* @return
* The PKCE verifier used with the identity provider
*/
public String getVerifier(String identifier) {
OpenIDAuthenticationSession session = resume(identifier);
if (session != null)
return session.getVerifier();
return null;
}
}

Loading