mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +00:00
GUACAMOLE-210: Properly generate and validate nonces.
This commit is contained in:
@@ -25,6 +25,7 @@ import java.util.Arrays;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.apache.guacamole.auth.openid.conf.ConfigurationService;
|
||||
import org.apache.guacamole.auth.openid.form.TokenField;
|
||||
import org.apache.guacamole.auth.openid.token.NonceService;
|
||||
import org.apache.guacamole.auth.openid.token.TokenValidationService;
|
||||
import org.apache.guacamole.auth.openid.user.AuthenticatedUser;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
@@ -52,6 +53,12 @@ public class AuthenticationProviderService {
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Service for validating and generating unique nonce values.
|
||||
*/
|
||||
@Inject
|
||||
private NonceService nonceService;
|
||||
|
||||
/**
|
||||
* Service for validating received ID tokens.
|
||||
*/
|
||||
@@ -112,7 +119,8 @@ public class AuthenticationProviderService {
|
||||
new TokenField(
|
||||
confService.getAuthorizationEndpoint(),
|
||||
confService.getClientID(),
|
||||
confService.getRedirectURI()
|
||||
confService.getRedirectURI(),
|
||||
nonceService.generate(30000 /* FIXME: Calculate appropriate value based on configuration */)
|
||||
)
|
||||
|
||||
}))
|
||||
|
@@ -21,6 +21,7 @@ package org.apache.guacamole.auth.openid;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import org.apache.guacamole.auth.openid.conf.ConfigurationService;
|
||||
import org.apache.guacamole.auth.openid.token.NonceService;
|
||||
import org.apache.guacamole.auth.openid.token.TokenValidationService;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.environment.Environment;
|
||||
@@ -74,6 +75,7 @@ public class OpenIDAuthenticationProviderModule extends AbstractModule {
|
||||
|
||||
// Bind openid-specific services
|
||||
bind(ConfigurationService.class);
|
||||
bind(NonceService.class);
|
||||
bind(TokenValidationService.class);
|
||||
|
||||
}
|
||||
|
@@ -20,15 +20,12 @@
|
||||
package org.apache.guacamole.auth.openid.form;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URLEncoder;
|
||||
import java.security.SecureRandom;
|
||||
import org.apache.guacamole.form.Field;
|
||||
|
||||
/**
|
||||
* Field definition which represents the token returned by an OpenID service.
|
||||
* Within the user interface, this will be rendered as an appropriate "Log in
|
||||
* with ..." button which links to the OpenID service.
|
||||
* Field definition which represents the token returned by an OpenID Connect
|
||||
* service.
|
||||
*/
|
||||
public class TokenField extends Field {
|
||||
|
||||
@@ -44,29 +41,12 @@ public class TokenField extends Field {
|
||||
private final String authorizationURI;
|
||||
|
||||
/**
|
||||
* Cryptographically-secure random number generator for generating the
|
||||
* required nonce.
|
||||
*/
|
||||
private static final SecureRandom random = new SecureRandom();
|
||||
|
||||
/**
|
||||
* Generates a cryptographically-secure nonce value. The nonce is intended
|
||||
* to be used to prevent replay attacks.
|
||||
*
|
||||
* @return
|
||||
* A cryptographically-secure nonce value.
|
||||
*/
|
||||
private static String generateNonce() {
|
||||
return new BigInteger(130, random).toString(32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new OpenID "id_token" field which links to the given OpenID
|
||||
* service using the provided client ID. Successful authentication at the
|
||||
* OpenID service will result in the client being redirected to the specified
|
||||
* redirect URI. The OpenID token will be embedded in the fragment (the part
|
||||
* following the hash symbol) of that URI, which the JavaScript side of
|
||||
* this extension will move to the query parameters.
|
||||
* Creates a new field which requests authentication via OpenID connect.
|
||||
* Successful authentication at the OpenID Connect service will result in
|
||||
* the client being redirected to the specified redirect URI. The OpenID
|
||||
* token will be embedded in the fragment (the part following the hash
|
||||
* symbol) of that URI, which the JavaScript side of this extension will
|
||||
* move to the query parameters.
|
||||
*
|
||||
* @param authorizationEndpoint
|
||||
* The full URL of the endpoint accepting OpenID authentication
|
||||
@@ -80,9 +60,13 @@ public class TokenField extends Field {
|
||||
* @param redirectURI
|
||||
* The URI that the OpenID service should redirect to upon successful
|
||||
* authentication.
|
||||
*
|
||||
* @param nonce
|
||||
* A random string unique to this request. To defend against replay
|
||||
* attacks, this value must cease being valid after its first use.
|
||||
*/
|
||||
public TokenField(String authorizationEndpoint, String clientID,
|
||||
String redirectURI) {
|
||||
String redirectURI, String nonce) {
|
||||
|
||||
// Init base field properties
|
||||
super(PARAMETER_NAME, "GUAC_OPENID_TOKEN");
|
||||
@@ -94,7 +78,7 @@ public class TokenField extends Field {
|
||||
+ "&response_type=id_token"
|
||||
+ "&client_id=" + URLEncoder.encode(clientID, "UTF-8")
|
||||
+ "&redirect_uri=" + URLEncoder.encode(redirectURI, "UTF-8")
|
||||
+ "&nonce=" + generateNonce();
|
||||
+ "&nonce=" + nonce;
|
||||
}
|
||||
|
||||
// Java is required to provide UTF-8 support
|
||||
|
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.token;
|
||||
|
||||
import com.google.inject.Singleton;
|
||||
import java.math.BigInteger;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Service for generating and validating single-use random tokens (nonces).
|
||||
*/
|
||||
@Singleton
|
||||
public class NonceService {
|
||||
|
||||
/**
|
||||
* Cryptographically-secure random number generator for generating the
|
||||
* required nonce.
|
||||
*/
|
||||
private final SecureRandom random = new SecureRandom();
|
||||
|
||||
/**
|
||||
* Map of all generated nonces to their corresponding expiration timestamps.
|
||||
* This Map must be periodically swept of expired nonces to avoid growing
|
||||
* without bound.
|
||||
*/
|
||||
private final Map<String, Long> nonces = new ConcurrentHashMap<String, Long>();
|
||||
|
||||
/**
|
||||
* The timestamp of the last expired nonce sweep.
|
||||
*/
|
||||
private long lastSweep = System.currentTimeMillis();
|
||||
|
||||
/**
|
||||
* The minimum amount of time to wait between sweeping expired nonces from
|
||||
* the Map.
|
||||
*/
|
||||
private static final long SWEEP_INTERVAL = 60000;
|
||||
|
||||
/**
|
||||
* Iterates through the entire Map of generated nonces, removing any nonce
|
||||
* that has exceeded its expiration timestamp. If insufficient time has
|
||||
* elapsed since the last sweep, as dictated by SWEEP_INTERVAL, this
|
||||
* function has no effect.
|
||||
*/
|
||||
private void sweepExpiredNonces() {
|
||||
|
||||
// Do not sweep until enough time has elapsed since the last sweep
|
||||
long currentTime = System.currentTimeMillis();
|
||||
if (currentTime - lastSweep < SWEEP_INTERVAL)
|
||||
return;
|
||||
|
||||
// Record time of sweep
|
||||
lastSweep = currentTime;
|
||||
|
||||
// For each stored nonce
|
||||
Iterator<Map.Entry<String, Long>> entries = nonces.entrySet().iterator();
|
||||
while (entries.hasNext()) {
|
||||
|
||||
// Remove all entries which have expired
|
||||
Map.Entry<String, Long> current = entries.next();
|
||||
if (current.getValue() <= System.currentTimeMillis())
|
||||
entries.remove();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cryptographically-secure nonce value. The nonce is intended
|
||||
* to be used to prevent replay attacks.
|
||||
*
|
||||
* @param maxAge
|
||||
* The maximum amount of time that the generated nonce should remain
|
||||
* valid, in milliseconds.
|
||||
*
|
||||
* @return
|
||||
* A cryptographically-secure nonce value.
|
||||
*/
|
||||
public String generate(long maxAge) {
|
||||
|
||||
// Sweep expired nonces if enough time has passed
|
||||
sweepExpiredNonces();
|
||||
|
||||
// Generate and store nonce, along with expiration timestamp
|
||||
String nonce = new BigInteger(130, random).toString(32);
|
||||
nonces.put(nonce, System.currentTimeMillis() + maxAge);
|
||||
return nonce;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the give nonce value is valid. A nonce is valid if and
|
||||
* only if it was generated by this instance of the NonceService. Testing
|
||||
* nonce validity through this function immediately and permanently
|
||||
* invalidates that nonce.
|
||||
*
|
||||
* @param nonce
|
||||
* The nonce value to test.
|
||||
*
|
||||
* @return
|
||||
* true if the provided nonce is valid, false otherwise.
|
||||
*/
|
||||
public boolean isValid(String nonce) {
|
||||
|
||||
// Remove nonce, verifying whether it was present at all
|
||||
Long expires = nonces.remove(nonce);
|
||||
if (expires == null)
|
||||
return false;
|
||||
|
||||
// Nonce is only valid if it hasn't expired
|
||||
return expires > System.currentTimeMillis();
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -51,6 +51,12 @@ public class TokenValidationService {
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Service for validating and generating unique nonce values.
|
||||
*/
|
||||
@Inject
|
||||
private NonceService nonceService;
|
||||
|
||||
/**
|
||||
* Validates and parses the given ID token, returning the username contained
|
||||
* therein, as defined by the username claim type given in
|
||||
@@ -91,6 +97,20 @@ public class TokenValidationService {
|
||||
// Validate JWT
|
||||
JwtClaims claims = jwtConsumer.processToClaims(token);
|
||||
|
||||
// Verify a nonce is present
|
||||
String nonce = claims.getStringClaimValue("nonce");
|
||||
if (nonce == null) {
|
||||
logger.info("Rejected OpenID token without nonce.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verify that we actually generated the nonce, and that it has not
|
||||
// already been used
|
||||
if (!nonceService.isValid(nonce)) {
|
||||
logger.debug("Rejected OpenID token with invalid/old nonce.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Pull username from claims
|
||||
String username = claims.getStringClaimValue(usernameClaim);
|
||||
if (username != null)
|
||||
|
Reference in New Issue
Block a user