mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +00:00
GUACAMOLE-839: Merge add webapp SSO support for certificates / smart cards.
This commit is contained in:
20
doc/licenses/bouncycastle-pkix-fips-1.0.7/LICENSE
Normal file
20
doc/licenses/bouncycastle-pkix-fips-1.0.7/LICENSE
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
Copyright (c) 2000 - 2021 The Legion of the Bouncy Castle Inc.
|
||||||
|
(https://www.bouncycastle.org)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
8
doc/licenses/bouncycastle-pkix-fips-1.0.7/README
Normal file
8
doc/licenses/bouncycastle-pkix-fips-1.0.7/README
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
BouncyCastle PKIX APIs, FIPS Distribution (https://www.bouncycastle.org/fips-java)
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Version: 1.0.7
|
||||||
|
From: 'The Legion of Bouncy Castle' (https://www.bouncycastle.org)
|
||||||
|
License(s):
|
||||||
|
MIT (bundled/bouncycastle-pkix-fips-1.0.7/LICENSE)
|
||||||
|
|
@@ -0,0 +1 @@
|
|||||||
|
org.bouncycastle:bcpkix-fips:jar:1.0.7
|
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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.sso;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of an in-progress authentication attempt.
|
||||||
|
*/
|
||||||
|
public class AuthenticationSession {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The absolute point in time after which this authentication session is
|
||||||
|
* invalid. This value is a UNIX epoch timestamp, as may be returned by
|
||||||
|
* {@link System#currentTimeMillis()}.
|
||||||
|
*/
|
||||||
|
private final long expirationTimestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new AuthenticationSession representing an in-progress
|
||||||
|
* authentication attempt.
|
||||||
|
*
|
||||||
|
* @param expires
|
||||||
|
* The number of milliseconds that may elapse before this session must
|
||||||
|
* be considered invalid.
|
||||||
|
*/
|
||||||
|
public AuthenticationSession(long expires) {
|
||||||
|
this.expirationTimestamp = System.currentTimeMillis() + expires;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether this authentication session is still valid (has not yet
|
||||||
|
* expired).
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* true if this authentication session is still valid, false if it has
|
||||||
|
* expired.
|
||||||
|
*/
|
||||||
|
public boolean isValid() {
|
||||||
|
return System.currentTimeMillis() < expirationTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -17,11 +17,10 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.apache.guacamole.auth.saml.acs;
|
package org.apache.guacamole.auth.sso;
|
||||||
|
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
@@ -29,14 +28,16 @@ import java.util.concurrent.ScheduledExecutorService;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manager service that temporarily stores SAML authentication attempts while
|
* Manager service that temporarily stores a user's authentication status while
|
||||||
* the authentication flow is underway. Authentication attempts are represented
|
* the authentication flow is underway. Authentication attempts are represented
|
||||||
* as temporary authentication sessions, allowing authentication attempts to
|
* as temporary authentication sessions, allowing authentication attempts to
|
||||||
* span multiple requests and redirects. Invalid or stale authentication
|
* span multiple requests, redirects, etc. Invalid or stale authentication
|
||||||
* sessions are automatically purged from storage.
|
* sessions are automatically purged from storage.
|
||||||
|
*
|
||||||
|
* @param <T>
|
||||||
|
* The type of sessions managed by this session manager.
|
||||||
*/
|
*/
|
||||||
@Singleton
|
public class AuthenticationSessionManager<T extends AuthenticationSession> {
|
||||||
public class AuthenticationSessionManager {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generator of arbitrary, unique, unpredictable identifiers.
|
* Generator of arbitrary, unique, unpredictable identifiers.
|
||||||
@@ -48,8 +49,7 @@ public class AuthenticationSessionManager {
|
|||||||
* Map of authentication session identifiers to their associated
|
* Map of authentication session identifiers to their associated
|
||||||
* {@link AuthenticationSession}.
|
* {@link AuthenticationSession}.
|
||||||
*/
|
*/
|
||||||
private final ConcurrentMap<String, AuthenticationSession> sessions =
|
private final ConcurrentMap<String, T> sessions = new ConcurrentHashMap<>();
|
||||||
new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executor service which runs the periodic cleanup task
|
* Executor service which runs the periodic cleanup task
|
||||||
@@ -59,7 +59,7 @@ public class AuthenticationSessionManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new AuthenticationSessionManager that manages in-progress
|
* Creates a new AuthenticationSessionManager that manages in-progress
|
||||||
* SAML authentication attempts. Invalid, stale sessions are automatically
|
* authentication attempts. Invalid, stale sessions are automatically
|
||||||
* cleaned up.
|
* cleaned up.
|
||||||
*/
|
*/
|
||||||
public AuthenticationSessionManager() {
|
public AuthenticationSessionManager() {
|
||||||
@@ -68,6 +68,20 @@ public class AuthenticationSessionManager {
|
|||||||
}, 1, 1, TimeUnit.MINUTES);
|
}, 1, 1, TimeUnit.MINUTES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a cryptographically-secure value identical in form to the
|
||||||
|
* session tokens generated by {@link #defer(org.apache.guacamole.auth.sso.AuthenticationSession)}
|
||||||
|
* but invalid. The returned value is indistinguishable from a valid token,
|
||||||
|
* but is not a valid token.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An invalid token value that is indistinguishable from a valid
|
||||||
|
* token.
|
||||||
|
*/
|
||||||
|
public String generateInvalid() {
|
||||||
|
return idGenerator.generateIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resumes the Guacamole side of the authentication process that was
|
* Resumes the Guacamole side of the authentication process that was
|
||||||
* previously deferred through a call to defer(). Once invoked, the
|
* previously deferred through a call to defer(). Once invoked, the
|
||||||
@@ -82,10 +96,10 @@ public class AuthenticationSessionManager {
|
|||||||
* was invoked, or null if the session is no longer valid or no such
|
* was invoked, or null if the session is no longer valid or no such
|
||||||
* value was returned by defer().
|
* value was returned by defer().
|
||||||
*/
|
*/
|
||||||
public AuthenticationSession resume(String identifier) {
|
public T resume(String identifier) {
|
||||||
|
|
||||||
if (identifier != null) {
|
if (identifier != null) {
|
||||||
AuthenticationSession session = sessions.remove(identifier);
|
T session = sessions.remove(identifier);
|
||||||
if (session != null && session.isValid())
|
if (session != null && session.isValid())
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
@@ -94,32 +108,6 @@ public class AuthenticationSessionManager {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the identity finally asserted by the SAML IdP at the end of the
|
|
||||||
* authentication process represented by the authentication session with
|
|
||||||
* the given identifier. If there is no such authentication session, or no
|
|
||||||
* valid identity has been asserted by the SAML IdP for that session, null
|
|
||||||
* is returned.
|
|
||||||
*
|
|
||||||
* @param identifier
|
|
||||||
* The unique string returned by the call to defer(). For convenience,
|
|
||||||
* this value may safely be null.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
* The identity finally asserted by the SAML IdP at the end of the
|
|
||||||
* authentication process represented by the authentication session
|
|
||||||
* with the given identifier, or null if there is no such identity.
|
|
||||||
*/
|
|
||||||
public AssertedIdentity getIdentity(String identifier) {
|
|
||||||
|
|
||||||
AuthenticationSession session = resume(identifier);
|
|
||||||
if (session != null)
|
|
||||||
return session.getIdentity();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defers the Guacamole side of authentication for the user having the
|
* Defers the Guacamole side of authentication for the user having the
|
||||||
* given authentication session such that it may be later resumed through a
|
* given authentication session such that it may be later resumed through a
|
||||||
@@ -129,14 +117,14 @@ public class AuthenticationSessionManager {
|
|||||||
* This method will automatically generate a new identifier.
|
* This method will automatically generate a new identifier.
|
||||||
*
|
*
|
||||||
* @param session
|
* @param session
|
||||||
* The {@link AuthenticationSession} representing the in-progress SAML
|
* The {@link AuthenticationSession} representing the in-progress
|
||||||
* authentication attempt.
|
* authentication attempt.
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
* A unique and unpredictable string that may be used to represent the
|
* A unique and unpredictable string that may be used to represent the
|
||||||
* given session when calling resume().
|
* given session when calling resume().
|
||||||
*/
|
*/
|
||||||
public String defer(AuthenticationSession session) {
|
public String defer(T session) {
|
||||||
String identifier = idGenerator.generateIdentifier();
|
String identifier = idGenerator.generateIdentifier();
|
||||||
sessions.put(identifier, session);
|
sessions.put(identifier, session);
|
||||||
return identifier;
|
return identifier;
|
||||||
@@ -152,20 +140,20 @@ public class AuthenticationSessionManager {
|
|||||||
* or similar unique identifier.
|
* or similar unique identifier.
|
||||||
*
|
*
|
||||||
* @param session
|
* @param session
|
||||||
* The {@link AuthenticationSession} representing the in-progress SAML
|
* The {@link AuthenticationSession} representing the in-progress
|
||||||
* authentication attempt.
|
* authentication attempt.
|
||||||
*
|
*
|
||||||
* @param identifier
|
* @param identifier
|
||||||
* A unique and unpredictable string that may be used to represent the
|
* A unique and unpredictable string that may be used to represent the
|
||||||
* given session when calling resume().
|
* given session when calling resume().
|
||||||
*/
|
*/
|
||||||
public void defer(AuthenticationSession session, String identifier) {
|
public void defer(T session, String identifier) {
|
||||||
sessions.put(identifier, session);
|
sessions.put(identifier, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuts down the executor service that periodically removes all invalid
|
* Shuts down the executor service that periodically removes all invalid
|
||||||
* authentication sessions. This must be invoked when the SAML extension is
|
* authentication sessions. This must be invoked when the auth extension is
|
||||||
* shut down in order to avoid resource leaks.
|
* shut down in order to avoid resource leaks.
|
||||||
*/
|
*/
|
||||||
public void shutdown() {
|
public void shutdown() {
|
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* 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.sso;
|
||||||
|
|
||||||
|
import com.google.common.io.BaseEncoding;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generator of unique and unpredictable identifiers. Each generated identifier
|
||||||
|
* is an arbitrary, random string produced using a cryptographically-secure
|
||||||
|
* random number generator.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class IdentifierGenerator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cryptographically-secure random number generator for generating unique
|
||||||
|
* identifiers.
|
||||||
|
*/
|
||||||
|
private final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique and unpredictable identifier. Each identifier is at
|
||||||
|
* least 256-bit and produced using a cryptographically-secure random
|
||||||
|
* number generator. The identifier may contain characters that differ only
|
||||||
|
* in case.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A unique and unpredictable identifier with at least 256 bits of
|
||||||
|
* entropy.
|
||||||
|
*/
|
||||||
|
public String generateIdentifier() {
|
||||||
|
return generateIdentifier(256);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique and unpredictable identifier having at least the
|
||||||
|
* given number of bits of entropy. The resulting identifier may have more
|
||||||
|
* than the number of bits required. The identifier may contain characters
|
||||||
|
* that differ only in case.
|
||||||
|
*
|
||||||
|
* @param minBits
|
||||||
|
* The number of bits of entropy that the identifier should contain.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A unique and unpredictable identifier with at least the given number
|
||||||
|
* of bits of entropy.
|
||||||
|
*/
|
||||||
|
public String generateIdentifier(int minBits) {
|
||||||
|
return generateIdentifier(minBits, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique and unpredictable identifier having at least the
|
||||||
|
* given number of bits of entropy. The resulting identifier may have more
|
||||||
|
* than the number of bits required. The identifier may contain characters
|
||||||
|
* that differ only in case.
|
||||||
|
*
|
||||||
|
* @param minBits
|
||||||
|
* The number of bits of entropy that the identifier should contain.
|
||||||
|
*
|
||||||
|
* @param caseSensitive
|
||||||
|
* Whether identifiers are permitted to contain characters that vary
|
||||||
|
* by case. If false, all characters that may vary by case will be
|
||||||
|
* lowercase, and the generated identifier will be longer.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A unique and unpredictable identifier with at least the given number
|
||||||
|
* of bits of entropy.
|
||||||
|
*/
|
||||||
|
public String generateIdentifier(int minBits, boolean caseSensitive) {
|
||||||
|
|
||||||
|
// Generate a base64 identifier if we're allowed to vary by case
|
||||||
|
if (caseSensitive) {
|
||||||
|
int minBytes = (minBits + 23) / 24 * 3; // Round up to nearest multiple of 3 bytes, as base64 encodes blocks of 3 bytes at a time
|
||||||
|
byte[] bytes = new byte[minBytes];
|
||||||
|
secureRandom.nextBytes(bytes);
|
||||||
|
return BaseEncoding.base64().encode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate base32 identifiers if we cannot vary by case
|
||||||
|
minBits = (minBits + 4) / 5 * 5; // Round up to nearest multiple of 5 bits, as base32 encodes 5 bits at a time
|
||||||
|
return new BigInteger(minBits, secureRandom).toString(32);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -17,39 +17,43 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.apache.guacamole.auth.openid.token;
|
package org.apache.guacamole.auth.sso;
|
||||||
|
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Inject;
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for generating and validating single-use random tokens (nonces).
|
* Service for generating and validating single-use random tokens (nonces).
|
||||||
|
* Each generated nonce is at least 128 bits and case-insensitive.
|
||||||
*/
|
*/
|
||||||
@Singleton
|
|
||||||
public class NonceService {
|
public class NonceService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cryptographically-secure random number generator for generating the
|
* Generator of arbitrary, unique, unpredictable identifiers.
|
||||||
* required nonce.
|
|
||||||
*/
|
*/
|
||||||
private final SecureRandom random = new SecureRandom();
|
@Inject
|
||||||
|
private IdentifierGenerator idGenerator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map of all generated nonces to their corresponding expiration timestamps.
|
* Map of all generated nonces to their corresponding expiration timestamps.
|
||||||
* This Map must be periodically swept of expired nonces to avoid growing
|
* This Map must be periodically swept of expired nonces to avoid growing
|
||||||
* without bound.
|
* without bound.
|
||||||
*/
|
*/
|
||||||
private final Map<String, Long> nonces = new ConcurrentHashMap<String, Long>();
|
private final Map<String, Long> nonces = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The timestamp of the last expired nonce sweep.
|
* The timestamp of the last expired nonce sweep.
|
||||||
*/
|
*/
|
||||||
private long lastSweep = System.currentTimeMillis();
|
private long lastSweep = System.currentTimeMillis();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum number of bits of entropy to include in each nonce.
|
||||||
|
*/
|
||||||
|
private static final int NONCE_BITS = 128;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The minimum amount of time to wait between sweeping expired nonces from
|
* The minimum amount of time to wait between sweeping expired nonces from
|
||||||
* the Map.
|
* the Map.
|
||||||
@@ -94,7 +98,8 @@ public class NonceService {
|
|||||||
* valid, in milliseconds.
|
* valid, in milliseconds.
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
* A cryptographically-secure nonce value.
|
* A cryptographically-secure nonce value. Generated nonces are at
|
||||||
|
* least 128-bit and are case-insensitive.
|
||||||
*/
|
*/
|
||||||
public String generate(long maxAge) {
|
public String generate(long maxAge) {
|
||||||
|
|
||||||
@@ -102,7 +107,7 @@ public class NonceService {
|
|||||||
sweepExpiredNonces();
|
sweepExpiredNonces();
|
||||||
|
|
||||||
// Generate and store nonce, along with expiration timestamp
|
// Generate and store nonce, along with expiration timestamp
|
||||||
String nonce = new BigInteger(130, random).toString(32);
|
String nonce = idGenerator.generateIdentifier(NONCE_BITS, false);
|
||||||
nonces.put(nonce, System.currentTimeMillis() + maxAge);
|
nonces.put(nonce, System.currentTimeMillis() + maxAge);
|
||||||
return nonce;
|
return nonce;
|
||||||
|
|
||||||
@@ -115,15 +120,20 @@ public class NonceService {
|
|||||||
* invalidates that nonce.
|
* invalidates that nonce.
|
||||||
*
|
*
|
||||||
* @param nonce
|
* @param nonce
|
||||||
* The nonce value to test.
|
* The nonce value to test. This value may be null, which will be
|
||||||
|
* considered an invalid nonce. Comparisons are case-insensitive.
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
* true if the provided nonce is valid, false otherwise.
|
* true if the provided nonce is valid, false otherwise.
|
||||||
*/
|
*/
|
||||||
public boolean isValid(String nonce) {
|
public boolean isValid(String nonce) {
|
||||||
|
|
||||||
|
// All null nonces are invalid.
|
||||||
|
if (nonce == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
// Remove nonce, verifying whether it was present at all
|
// Remove nonce, verifying whether it was present at all
|
||||||
Long expires = nonces.remove(nonce);
|
Long expires = nonces.remove(nonce.toLowerCase(Locale.US));
|
||||||
if (expires == null)
|
if (expires == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
@@ -12,6 +12,10 @@
|
|||||||
"NAME" : "SAML SSO Backend"
|
"NAME" : "SAML SSO Backend"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"DATA_SOURCE_SSL" : {
|
||||||
|
"NAME" : "SSL/TLS SSO Backend"
|
||||||
|
},
|
||||||
|
|
||||||
"LOGIN" : {
|
"LOGIN" : {
|
||||||
"FIELD_HEADER_ID_TOKEN" : "",
|
"FIELD_HEADER_ID_TOKEN" : "",
|
||||||
"FIELD_HEADER_STATE" : "",
|
"FIELD_HEADER_STATE" : "",
|
||||||
@@ -20,6 +24,7 @@
|
|||||||
"NAME_IDP_CAS" : "CAS",
|
"NAME_IDP_CAS" : "CAS",
|
||||||
"NAME_IDP_OPENID" : "OpenID",
|
"NAME_IDP_OPENID" : "OpenID",
|
||||||
"NAME_IDP_SAML" : "SAML",
|
"NAME_IDP_SAML" : "SAML",
|
||||||
|
"NAME_IDP_SSL" : "Certificate / Smart Card",
|
||||||
"SECTION_HEADER_SSO_OPTIONS" : "Sign in with:"
|
"SECTION_HEADER_SSO_OPTIONS" : "Sign in with:"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -59,6 +59,13 @@
|
|||||||
<version>1.5.0</version>
|
<version>1.5.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SSL Authentication Extension -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.guacamole</groupId>
|
||||||
|
<artifactId>guacamole-auth-sso-ssl</artifactId>
|
||||||
|
<version>1.5.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
@@ -60,6 +60,15 @@
|
|||||||
</includes>
|
</includes>
|
||||||
</dependencySet>
|
</dependencySet>
|
||||||
|
|
||||||
|
<!-- SSL extension .jar -->
|
||||||
|
<dependencySet>
|
||||||
|
<outputDirectory>ssl</outputDirectory>
|
||||||
|
<useProjectArtifact>false</useProjectArtifact>
|
||||||
|
<includes>
|
||||||
|
<include>org.apache.guacamole:guacamole-auth-sso-ssl</include>
|
||||||
|
</includes>
|
||||||
|
</dependencySet>
|
||||||
|
|
||||||
</dependencySets>
|
</dependencySets>
|
||||||
|
|
||||||
<!-- Include extension licenses -->
|
<!-- Include extension licenses -->
|
||||||
|
@@ -29,9 +29,9 @@ import java.util.Set;
|
|||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import org.apache.guacamole.auth.openid.conf.ConfigurationService;
|
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.auth.openid.token.TokenValidationService;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.auth.sso.NonceService;
|
||||||
import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService;
|
import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService;
|
||||||
import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser;
|
import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser;
|
||||||
import org.apache.guacamole.form.Field;
|
import org.apache.guacamole.form.Field;
|
||||||
|
@@ -20,8 +20,9 @@
|
|||||||
package org.apache.guacamole.auth.openid;
|
package org.apache.guacamole.auth.openid;
|
||||||
|
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Scopes;
|
||||||
import org.apache.guacamole.auth.openid.conf.ConfigurationService;
|
import org.apache.guacamole.auth.openid.conf.ConfigurationService;
|
||||||
import org.apache.guacamole.auth.openid.token.NonceService;
|
import org.apache.guacamole.auth.sso.NonceService;
|
||||||
import org.apache.guacamole.auth.openid.token.TokenValidationService;
|
import org.apache.guacamole.auth.openid.token.TokenValidationService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,7 +33,7 @@ public class OpenIDAuthenticationProviderModule extends AbstractModule {
|
|||||||
@Override
|
@Override
|
||||||
protected void configure() {
|
protected void configure() {
|
||||||
bind(ConfigurationService.class);
|
bind(ConfigurationService.class);
|
||||||
bind(NonceService.class);
|
bind(NonceService.class).in(Scopes.SINGLETON);
|
||||||
bind(TokenValidationService.class);
|
bind(TokenValidationService.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -26,6 +26,7 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.apache.guacamole.auth.openid.conf.ConfigurationService;
|
import org.apache.guacamole.auth.openid.conf.ConfigurationService;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.auth.sso.NonceService;
|
||||||
import org.jose4j.jwk.HttpsJwks;
|
import org.jose4j.jwk.HttpsJwks;
|
||||||
import org.jose4j.jwt.JwtClaims;
|
import org.jose4j.jwt.JwtClaims;
|
||||||
import org.jose4j.jwt.MalformedClaimException;
|
import org.jose4j.jwt.MalformedClaimException;
|
||||||
|
@@ -28,7 +28,7 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
import org.apache.guacamole.auth.saml.user.SAMLAuthenticatedUser;
|
import org.apache.guacamole.auth.saml.user.SAMLAuthenticatedUser;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
import org.apache.guacamole.auth.saml.acs.AssertedIdentity;
|
import org.apache.guacamole.auth.saml.acs.AssertedIdentity;
|
||||||
import org.apache.guacamole.auth.saml.acs.AuthenticationSessionManager;
|
import org.apache.guacamole.auth.saml.acs.SAMLAuthenticationSessionManager;
|
||||||
import org.apache.guacamole.auth.saml.acs.SAMLService;
|
import org.apache.guacamole.auth.saml.acs.SAMLService;
|
||||||
import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService;
|
import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService;
|
||||||
import org.apache.guacamole.form.Field;
|
import org.apache.guacamole.form.Field;
|
||||||
@@ -61,7 +61,7 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS
|
|||||||
* Manager of active SAML authentication attempts.
|
* Manager of active SAML authentication attempts.
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
private AuthenticationSessionManager sessionManager;
|
private SAMLAuthenticationSessionManager sessionManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for processing SAML requests/responses.
|
* Service for processing SAML requests/responses.
|
||||||
|
@@ -22,8 +22,7 @@ package org.apache.guacamole.auth.saml;
|
|||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
|
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
|
||||||
import org.apache.guacamole.auth.saml.acs.AssertionConsumerServiceResource;
|
import org.apache.guacamole.auth.saml.acs.AssertionConsumerServiceResource;
|
||||||
import org.apache.guacamole.auth.saml.acs.AuthenticationSessionManager;
|
import org.apache.guacamole.auth.saml.acs.SAMLAuthenticationSessionManager;
|
||||||
import org.apache.guacamole.auth.saml.acs.IdentifierGenerator;
|
|
||||||
import org.apache.guacamole.auth.saml.acs.SAMLService;
|
import org.apache.guacamole.auth.saml.acs.SAMLService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,9 +33,8 @@ public class SAMLAuthenticationProviderModule extends AbstractModule {
|
|||||||
@Override
|
@Override
|
||||||
protected void configure() {
|
protected void configure() {
|
||||||
bind(AssertionConsumerServiceResource.class);
|
bind(AssertionConsumerServiceResource.class);
|
||||||
bind(AuthenticationSessionManager.class);
|
|
||||||
bind(ConfigurationService.class);
|
bind(ConfigurationService.class);
|
||||||
bind(IdentifierGenerator.class);
|
bind(SAMLAuthenticationSessionManager.class);
|
||||||
bind(SAMLService.class);
|
bind(SAMLService.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -56,7 +56,7 @@ public class AssertionConsumerServiceResource extends SSOResource {
|
|||||||
* Manager of active SAML authentication attempts.
|
* Manager of active SAML authentication attempts.
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
private AuthenticationSessionManager sessionManager;
|
private SAMLAuthenticationSessionManager sessionManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for processing SAML requests/responses.
|
* Service for processing SAML requests/responses.
|
||||||
@@ -107,7 +107,7 @@ public class AssertionConsumerServiceResource extends SSOResource {
|
|||||||
try {
|
try {
|
||||||
|
|
||||||
// Validate and parse identity asserted by SAML IdP
|
// Validate and parse identity asserted by SAML IdP
|
||||||
AuthenticationSession session = saml.processResponse(
|
SAMLAuthenticationSession session = saml.processResponse(
|
||||||
consumedRequest.getRequestURL().toString(),
|
consumedRequest.getRequestURL().toString(),
|
||||||
relayState, samlResponse);
|
relayState, samlResponse);
|
||||||
|
|
||||||
|
@@ -1,54 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.saml.acs;
|
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding;
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generator of unique and unpredictable identifiers. Each generated identifier
|
|
||||||
* is an arbitrary, random string produced using a cryptographically-secure
|
|
||||||
* random number generator and consists of at least 256 bits.
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
public class IdentifierGenerator {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cryptographically-secure random number generator for generating unique
|
|
||||||
* identifiers.
|
|
||||||
*/
|
|
||||||
private final SecureRandom secureRandom = new SecureRandom();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a unique and unpredictable identifier. Each identifier is at
|
|
||||||
* least 256-bit and produced using a cryptographically-secure random
|
|
||||||
* number generator.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
* A unique and unpredictable identifier.
|
|
||||||
*/
|
|
||||||
public String generateIdentifier() {
|
|
||||||
byte[] bytes = new byte[33];
|
|
||||||
secureRandom.nextBytes(bytes);
|
|
||||||
return BaseEncoding.base64().encode(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -19,17 +19,12 @@
|
|||||||
|
|
||||||
package org.apache.guacamole.auth.saml.acs;
|
package org.apache.guacamole.auth.saml.acs;
|
||||||
|
|
||||||
|
import org.apache.guacamole.auth.sso.AuthenticationSession;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of an in-progress SAML authentication attempt.
|
* Representation of an in-progress SAML authentication attempt.
|
||||||
*/
|
*/
|
||||||
public class AuthenticationSession {
|
public class SAMLAuthenticationSession extends AuthenticationSession {
|
||||||
|
|
||||||
/**
|
|
||||||
* The absolute point in time after which this authentication session is
|
|
||||||
* invalid. This value is a UNIX epoch timestamp, as may be returned by
|
|
||||||
* {@link System#currentTimeMillis()}.
|
|
||||||
*/
|
|
||||||
private final long expirationTimestamp;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The request ID of the SAML request associated with the authentication
|
* The request ID of the SAML request associated with the authentication
|
||||||
@@ -55,24 +50,21 @@ public class AuthenticationSession {
|
|||||||
* The number of milliseconds that may elapse before this session must
|
* The number of milliseconds that may elapse before this session must
|
||||||
* be considered invalid.
|
* be considered invalid.
|
||||||
*/
|
*/
|
||||||
public AuthenticationSession(String requestId, long expires) {
|
public SAMLAuthenticationSession(String requestId, long expires) {
|
||||||
this.expirationTimestamp = System.currentTimeMillis() + expires;
|
super(expires);
|
||||||
this.requestId = requestId;
|
this.requestId = requestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether this authentication session is still valid (has not yet
|
* {@inheritDoc}
|
||||||
* expired). If an identity has been asserted by the SAML IdP, this
|
*
|
||||||
|
* <p>If an identity has been asserted by the SAML IdP, this
|
||||||
* considers also whether the SAML response asserting that identity has
|
* considers also whether the SAML response asserting that identity has
|
||||||
* expired.
|
* expired.
|
||||||
*
|
|
||||||
* @return
|
|
||||||
* true if this authentication session is still valid, false if it has
|
|
||||||
* expired.
|
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public boolean isValid() {
|
public boolean isValid() {
|
||||||
return System.currentTimeMillis() < expirationTimestamp
|
return super.isValid() && (identity == null || identity.isValid());
|
||||||
&& (identity == null || identity.isValid());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* 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.saml.acs;
|
||||||
|
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import org.apache.guacamole.auth.sso.AuthenticationSessionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager service that temporarily stores SAML authentication attempts while
|
||||||
|
* the authentication flow is underway.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class SAMLAuthenticationSessionManager
|
||||||
|
extends AuthenticationSessionManager<SAMLAuthenticationSession> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the identity finally asserted by the SAML IdP at the end of the
|
||||||
|
* authentication process represented by the authentication session with
|
||||||
|
* the given identifier. If there is no such authentication session, or no
|
||||||
|
* valid identity has been asserted by the SAML IdP for that session, null
|
||||||
|
* is returned.
|
||||||
|
*
|
||||||
|
* @param identifier
|
||||||
|
* The unique string returned by the call to defer(). For convenience,
|
||||||
|
* this value may safely be null.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The identity finally asserted by the SAML IdP at the end of the
|
||||||
|
* authentication process represented by the authentication session
|
||||||
|
* with the given identifier, or null if there is no such identity.
|
||||||
|
*/
|
||||||
|
public AssertedIdentity getIdentity(String identifier) {
|
||||||
|
|
||||||
|
SAMLAuthenticationSession session = resume(identifier);
|
||||||
|
if (session != null)
|
||||||
|
return session.getIdentity();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -22,7 +22,6 @@ package org.apache.guacamole.auth.saml.acs;
|
|||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import com.onelogin.saml2.Auth;
|
import com.onelogin.saml2.Auth;
|
||||||
import com.onelogin.saml2.authn.AuthnRequest;
|
|
||||||
import com.onelogin.saml2.authn.AuthnRequestParams;
|
import com.onelogin.saml2.authn.AuthnRequestParams;
|
||||||
import com.onelogin.saml2.authn.SamlResponse;
|
import com.onelogin.saml2.authn.SamlResponse;
|
||||||
import com.onelogin.saml2.exception.SettingsException;
|
import com.onelogin.saml2.exception.SettingsException;
|
||||||
@@ -37,6 +36,7 @@ import org.apache.guacamole.GuacamoleException;
|
|||||||
import org.apache.guacamole.GuacamoleSecurityException;
|
import org.apache.guacamole.GuacamoleSecurityException;
|
||||||
import org.apache.guacamole.GuacamoleServerException;
|
import org.apache.guacamole.GuacamoleServerException;
|
||||||
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
|
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
|
||||||
|
import org.apache.guacamole.auth.sso.IdentifierGenerator;
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,7 +56,7 @@ public class SAMLService {
|
|||||||
* Manager of active SAML authentication attempts.
|
* Manager of active SAML authentication attempts.
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
private AuthenticationSessionManager sessionManager;
|
private SAMLAuthenticationSessionManager sessionManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generator of arbitrary, unique, unpredictable identifiers.
|
* Generator of arbitrary, unique, unpredictable identifiers.
|
||||||
@@ -99,7 +99,7 @@ public class SAMLService {
|
|||||||
|
|
||||||
// Create a new authentication session to represent this attempt while
|
// Create a new authentication session to represent this attempt while
|
||||||
// it is in progress, using the request ID that was just issued
|
// it is in progress, using the request ID that was just issued
|
||||||
AuthenticationSession session = new AuthenticationSession(
|
SAMLAuthenticationSession session = new SAMLAuthenticationSession(
|
||||||
auth.getLastRequestId(),
|
auth.getLastRequestId(),
|
||||||
confService.getAuthenticationTimeout() * 60000L);
|
confService.getAuthenticationTimeout() * 60000L);
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ public class SAMLService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes the given SAML response, as received by the SAML ACS endpoint
|
* Processes the given SAML response, as received by the SAML ACS endpoint
|
||||||
* at the given URL, producing an {@link AuthenticationSession} that now
|
* at the given URL, producing an {@link SAMLAuthenticationSession} that now
|
||||||
* includes a valid assertion of the user's identity. If the SAML response
|
* includes a valid assertion of the user's identity. If the SAML response
|
||||||
* is invalid in any way, an exception is thrown.
|
* is invalid in any way, an exception is thrown.
|
||||||
*
|
*
|
||||||
@@ -148,7 +148,7 @@ public class SAMLService {
|
|||||||
* given URL.
|
* given URL.
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
* The {@link AuthenticationSession} associated with the in-progress
|
* The {@link SAMLAuthenticationSession} associated with the in-progress
|
||||||
* authentication attempt, now associated with the {@link AssertedIdentity}
|
* authentication attempt, now associated with the {@link AssertedIdentity}
|
||||||
* representing the identity of the user asserted by the SAML IdP.
|
* representing the identity of the user asserted by the SAML IdP.
|
||||||
*
|
*
|
||||||
@@ -157,14 +157,14 @@ public class SAMLService {
|
|||||||
* information required to validate or decrypt the response cannot be
|
* information required to validate or decrypt the response cannot be
|
||||||
* read.
|
* read.
|
||||||
*/
|
*/
|
||||||
public AuthenticationSession processResponse(String url, String relayState,
|
public SAMLAuthenticationSession processResponse(String url, String relayState,
|
||||||
String encodedResponse) throws GuacamoleException {
|
String encodedResponse) throws GuacamoleException {
|
||||||
|
|
||||||
if (relayState == null)
|
if (relayState == null)
|
||||||
throw new GuacamoleSecurityException("\"RelayState\" value "
|
throw new GuacamoleSecurityException("\"RelayState\" value "
|
||||||
+ "is missing from SAML response.");
|
+ "is missing from SAML response.");
|
||||||
|
|
||||||
AuthenticationSession session = sessionManager.resume(relayState);
|
SAMLAuthenticationSession session = sessionManager.resume(relayState);
|
||||||
if (session == null)
|
if (session == null)
|
||||||
throw new GuacamoleSecurityException("\"RelayState\" value "
|
throw new GuacamoleSecurityException("\"RelayState\" value "
|
||||||
+ "included with SAML response is not valid.");
|
+ "included with SAML response is not valid.");
|
||||||
|
3
extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.gitignore
vendored
Normal file
3
extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
*~
|
||||||
|
target/
|
||||||
|
src/main/resources/generated/
|
@@ -0,0 +1 @@
|
|||||||
|
src/main/resources/html/*.html
|
@@ -0,0 +1,131 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>org.apache.guacamole</groupId>
|
||||||
|
<artifactId>guacamole-auth-sso-ssl</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<version>1.5.0</version>
|
||||||
|
<name>guacamole-auth-sso-ssl</name>
|
||||||
|
<url>http://guacamole.apache.org/</url>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.apache.guacamole</groupId>
|
||||||
|
<artifactId>guacamole-auth-sso</artifactId>
|
||||||
|
<version>1.5.0</version>
|
||||||
|
<relativePath>../../</relativePath>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
|
||||||
|
<!-- JS/CSS Minification Plugin -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.github.buckelieg</groupId>
|
||||||
|
<artifactId>minify-maven-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>default-cli</id>
|
||||||
|
<configuration>
|
||||||
|
<charset>UTF-8</charset>
|
||||||
|
|
||||||
|
<webappSourceDir>${basedir}/src/main/resources</webappSourceDir>
|
||||||
|
<webappTargetDir>${project.build.directory}/classes</webappTargetDir>
|
||||||
|
|
||||||
|
<jsSourceDir>/</jsSourceDir>
|
||||||
|
<jsTargetDir>/</jsTargetDir>
|
||||||
|
<jsFinalFile>ssl.js</jsFinalFile>
|
||||||
|
|
||||||
|
<jsSourceFiles>
|
||||||
|
<jsSourceFile>license.txt</jsSourceFile>
|
||||||
|
</jsSourceFiles>
|
||||||
|
|
||||||
|
<jsSourceIncludes>
|
||||||
|
<jsSourceInclude>**/*.js</jsSourceInclude>
|
||||||
|
</jsSourceIncludes>
|
||||||
|
|
||||||
|
<!-- Do not minify and include tests -->
|
||||||
|
<jsSourceExcludes>
|
||||||
|
<jsSourceExclude>**/*.test.js</jsSourceExclude>
|
||||||
|
</jsSourceExcludes>
|
||||||
|
<jsEngine>CLOSURE</jsEngine>
|
||||||
|
|
||||||
|
<!-- Disable warnings for JSDoc annotations -->
|
||||||
|
<closureWarningLevels>
|
||||||
|
<misplacedTypeAnnotation>OFF</misplacedTypeAnnotation>
|
||||||
|
<nonStandardJsDocs>OFF</nonStandardJsDocs>
|
||||||
|
</closureWarningLevels>
|
||||||
|
|
||||||
|
</configuration>
|
||||||
|
<goals>
|
||||||
|
<goal>minify</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
<dependencies>
|
||||||
|
|
||||||
|
<!-- Guacamole Extension API -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.guacamole</groupId>
|
||||||
|
<artifactId>guacamole-ext</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Core SSO support -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.guacamole</groupId>
|
||||||
|
<artifactId>guacamole-auth-sso-base</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Guice -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.inject</groupId>
|
||||||
|
<artifactId>guice</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Java servlet API -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.servlet</groupId>
|
||||||
|
<artifactId>servlet-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JAX-RS Annotations -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.ws.rs</groupId>
|
||||||
|
<artifactId>jsr311-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Use FIPS variant of Bouncy Castle crypto library -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcpkix-fips</artifactId>
|
||||||
|
<version>1.0.7</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
@@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* 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.ssl;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Collections;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import org.apache.guacamole.GuacamoleClientException;
|
||||||
|
import org.apache.guacamole.auth.ssl.conf.ConfigurationService;
|
||||||
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.GuacamoleResourceNotFoundException;
|
||||||
|
import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService;
|
||||||
|
import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser;
|
||||||
|
import org.apache.guacamole.net.auth.Credentials;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that authenticates Guacamole users using SSL/TLS authentication
|
||||||
|
* provided by an external SSL termination service.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class AuthenticationProviderService implements SSOAuthenticationProviderService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for retrieving configuration information.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private ConfigurationService confService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session manager for generating and maintaining unique tokens to
|
||||||
|
* represent the authentication flow of a user who has only partially
|
||||||
|
* authenticated. Here, these tokens represent a user that has been
|
||||||
|
* validated by SSL termination and allow the Guacamole instance that
|
||||||
|
* doesn't require SSL/TLS authentication to retrieve the user's identity
|
||||||
|
* and complete the authentication process.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private SSLAuthenticationSessionManager sessionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider for AuthenticatedUser objects.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private Provider<SSOAuthenticatedUser> authenticatedUserProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the query parameter containing the temporary session token
|
||||||
|
* representing the current state of an in-progress authentication attempt.
|
||||||
|
*/
|
||||||
|
private static final String AUTH_SESSION_PARAMETER_NAME = "state";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the given HTTP request, returning the identity represented by
|
||||||
|
* the auth session token present in that request. If no such token is
|
||||||
|
* present, or the token does not represent a valid identity, null is
|
||||||
|
* returned.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* The HTTP request to process.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The identity represented by the auth session token in the request,
|
||||||
|
* or null if there is no such token or the token does not represent a
|
||||||
|
* valid identity.
|
||||||
|
*/
|
||||||
|
private SSOAuthenticatedUser processIdentity(Credentials credentials, HttpServletRequest request) {
|
||||||
|
|
||||||
|
String state = request.getParameter(AUTH_SESSION_PARAMETER_NAME);
|
||||||
|
String username = sessionManager.getIdentity(state);
|
||||||
|
if (username == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
|
||||||
|
authenticatedUser.init(username, credentials,
|
||||||
|
Collections.emptySet(), Collections.emptyMap());
|
||||||
|
return authenticatedUser;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SSOAuthenticatedUser authenticateUser(Credentials credentials)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Overall flow:
|
||||||
|
//
|
||||||
|
// 1) An unauthenticated user makes a GET request to
|
||||||
|
// ".../api/ext/ssl/identity". After a series of redirects
|
||||||
|
// intended to prevent that identity from being inadvertently
|
||||||
|
// cached and inherited by future authentication attempts on the
|
||||||
|
// same client machine, an external SSL termination service requests
|
||||||
|
// and validates the user's certificate, those details are passed
|
||||||
|
// back to Guacamole via HTTP headers, and Guacamole produces a JSON
|
||||||
|
// response containing an opaque state value.
|
||||||
|
//
|
||||||
|
// 2) The user (still unauthenticated) resubmits the opaque state
|
||||||
|
// value from the received JSON as the "state" parameter of a
|
||||||
|
// standard Guacamole authentication request (".../api/tokens").
|
||||||
|
//
|
||||||
|
// 3) If the certificate received was valid, the user is authenticated
|
||||||
|
// according to the identity asserted by that certificate. If not,
|
||||||
|
// authentication is refused.
|
||||||
|
//
|
||||||
|
// NOTE: All SSL termination endpoints in front of Guacamole MUST
|
||||||
|
// be configured to drop these headers from any inbound requests
|
||||||
|
// or users may be able to assert arbitrary identities, since this
|
||||||
|
// extension does not validate anything but the certificate timestamps.
|
||||||
|
// It relies purely on SSL termination to validate that the certificate
|
||||||
|
// was signed by the expected CA.
|
||||||
|
//
|
||||||
|
|
||||||
|
// We can't authenticate using SSL/TLS client auth unless there's an
|
||||||
|
// associated HTTP request
|
||||||
|
HttpServletRequest request = credentials.getRequest();
|
||||||
|
if (request == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// We MUST have the domain associated with the request to ensure we
|
||||||
|
// always get fresh SSL sessions when validating client certificates
|
||||||
|
String host = request.getHeader("Host");
|
||||||
|
if (host == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Handle only auth session tokens at the primary URI, using the
|
||||||
|
// pre-verified information from those tokens to determine user
|
||||||
|
// identity.
|
||||||
|
//
|
||||||
|
|
||||||
|
if (confService.isPrimaryHostname(host))
|
||||||
|
return processIdentity(credentials, request);
|
||||||
|
|
||||||
|
// All other requests are not allowed - refuse to authenticate
|
||||||
|
throw new GuacamoleClientException("Direct authentication against "
|
||||||
|
+ "this endpoint is not valid without first requesting to "
|
||||||
|
+ "authenticate at the primary URL of this Guacamole "
|
||||||
|
+ "instance.");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URI getLoginURI() throws GuacamoleException {
|
||||||
|
throw new GuacamoleResourceNotFoundException("No such resource.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
sessionManager.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* 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.ssl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST API response that reports the result of attempting to authenticate the
|
||||||
|
* user using SSL/TLS client authentication. The information within this
|
||||||
|
* result is intentionally opaque and must be resubmitted in a separate
|
||||||
|
* authentication request for authentication to finally succeed or fail.
|
||||||
|
*/
|
||||||
|
public class OpaqueAuthenticationResult {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An arbitrary value representing the result of authenticating the
|
||||||
|
* current user.
|
||||||
|
*/
|
||||||
|
private final String state;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new OpaqueAuthenticationResult containing the given opaque
|
||||||
|
* state value. Successful authentication results must be indistinguishable
|
||||||
|
* from unsuccessful results with respect to this value. Only using this
|
||||||
|
* value within ANOTHER authentication attempt can determine whether
|
||||||
|
* authentication is successful.
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* An arbitrary value representing the result of authenticating the
|
||||||
|
* current user.
|
||||||
|
*/
|
||||||
|
public OpaqueAuthenticationResult(String state) {
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an arbitrary value representing the result of authenticating the
|
||||||
|
* current user. This value may be resubmitted as the "state" parameter of
|
||||||
|
* an authentication request beneath the primary URI of the web application
|
||||||
|
* to finalize the authentication procedure and determine whether the
|
||||||
|
* operation has succeeded or failed.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An arbitrary value representing the result of authenticating the
|
||||||
|
* current user.
|
||||||
|
*/
|
||||||
|
public String getState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* 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.ssl;
|
||||||
|
|
||||||
|
import org.apache.guacamole.auth.sso.SSOAuthenticationProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guacamole authentication backend which authenticates users using SSL/TLS
|
||||||
|
* client authentication provided by some external SSL termination system. This
|
||||||
|
* SSL termination system must be configured to provide access to this same
|
||||||
|
* instance of Guacamole and must have both a wildcard certificate and wildcard
|
||||||
|
* DNS. No storage for connections is provided - only authentication. Storage
|
||||||
|
* must be provided by some other extension.
|
||||||
|
*/
|
||||||
|
public class SSLAuthenticationProvider extends SSOAuthenticationProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new SSLAuthenticationProvider that authenticates users against
|
||||||
|
* an external SSL termination system using SSL/TLS client authentication.
|
||||||
|
*/
|
||||||
|
public SSLAuthenticationProvider() {
|
||||||
|
super(AuthenticationProviderService.class, SSLClientAuthenticationResource.class,
|
||||||
|
new SSLAuthenticationProviderModule());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIdentifier() {
|
||||||
|
return "ssl";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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.ssl;
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Scopes;
|
||||||
|
import org.apache.guacamole.auth.ssl.conf.ConfigurationService;
|
||||||
|
import org.apache.guacamole.auth.sso.NonceService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guice module which configures injections specific to SSO using SSL/TLS
|
||||||
|
* client authentication.
|
||||||
|
*/
|
||||||
|
public class SSLAuthenticationProviderModule extends AbstractModule {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
bind(ConfigurationService.class);
|
||||||
|
bind(NonceService.class).in(Scopes.SINGLETON);
|
||||||
|
bind(SSLAuthenticationSessionManager.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* 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.ssl;
|
||||||
|
|
||||||
|
import org.apache.guacamole.auth.sso.AuthenticationSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of an in-progress SSL/TLS authentication attempt.
|
||||||
|
*/
|
||||||
|
public class SSLAuthenticationSession extends AuthenticationSession {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identity asserted by the external SSL termination service.
|
||||||
|
*/
|
||||||
|
private final String identity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new AuthenticationSession representing an in-progress SSL/TLS
|
||||||
|
* authentication attempt.
|
||||||
|
*
|
||||||
|
* @param identity
|
||||||
|
* The identity asserted by the external SSL termination service. This
|
||||||
|
* MAY NOT be null.
|
||||||
|
*
|
||||||
|
* @param expires
|
||||||
|
* The number of milliseconds that may elapse before this session must
|
||||||
|
* be considered invalid.
|
||||||
|
*/
|
||||||
|
public SSLAuthenticationSession(String identity, long expires) {
|
||||||
|
super(expires);
|
||||||
|
this.identity = identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the identity asserted by the external SSL termination service.
|
||||||
|
* As authentication will have completed with respect to the SSL
|
||||||
|
* termination service by the time this session is created, this will
|
||||||
|
* always be non-null.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The identity asserted by the external SSL termination service.
|
||||||
|
*/
|
||||||
|
public String getIdentity() {
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* 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.ssl;
|
||||||
|
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import org.apache.guacamole.auth.sso.AuthenticationSessionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager service that temporarily stores SSL/TLS authentication attempts
|
||||||
|
* while the authentication flow is underway.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class SSLAuthenticationSessionManager
|
||||||
|
extends AuthenticationSessionManager<SSLAuthenticationSession> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the identity asserted by the external SSL termination service at
|
||||||
|
* the end of the authentication process represented by the authentication
|
||||||
|
* session with the given identifier. If there is no such authentication
|
||||||
|
* session, or no valid identity has been asserted for that session, null
|
||||||
|
* is returned.
|
||||||
|
*
|
||||||
|
* @param identifier
|
||||||
|
* The unique string returned by the call to defer(). For convenience,
|
||||||
|
* this value may safely be null.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The identity asserted by the external SSL termination service at the
|
||||||
|
* end of the authentication process represented by the authentication
|
||||||
|
* session with the given identifier, or null if there is no such
|
||||||
|
* identity.
|
||||||
|
*/
|
||||||
|
public String getIdentity(String identifier) {
|
||||||
|
|
||||||
|
SSLAuthenticationSession session = resume(identifier);
|
||||||
|
if (session != null)
|
||||||
|
return session.getIdentity();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,412 @@
|
|||||||
|
/*
|
||||||
|
* 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.ssl;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import javax.naming.InvalidNameException;
|
||||||
|
import javax.naming.ldap.LdapName;
|
||||||
|
import javax.naming.ldap.Rdn;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.HeaderParam;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import org.apache.guacamole.GuacamoleClientException;
|
||||||
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.GuacamoleResourceNotFoundException;
|
||||||
|
import org.apache.guacamole.GuacamoleServerException;
|
||||||
|
import org.apache.guacamole.auth.ssl.conf.ConfigurationService;
|
||||||
|
import org.apache.guacamole.auth.sso.NonceService;
|
||||||
|
import org.apache.guacamole.auth.sso.SSOResource;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.asn1.x500.style.RFC4519Style;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.openssl.PEMParser;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST API resource that allows the user to retrieve an opaque state value
|
||||||
|
* representing their identity as determined by SSL/TLS client authentication.
|
||||||
|
* The opaque value may represent a valid identity or an authentication
|
||||||
|
* failure, and must be resubmitted within a normal Guacamole authentication
|
||||||
|
* request to finalize the authentication process.
|
||||||
|
*/
|
||||||
|
public class SSLClientAuthenticationResource extends SSOResource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The string value that the SSL termination service uses for its client
|
||||||
|
* verification header to represent that the client certificate has been
|
||||||
|
* verified.
|
||||||
|
*/
|
||||||
|
private static final String CLIENT_VERIFIED_HEADER_SUCCESS_VALUE = "SUCCESS";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The string value that the SSL termination service uses for its client
|
||||||
|
* verification header to represent that the client certificate is absent.
|
||||||
|
*/
|
||||||
|
private static final String CLIENT_VERIFIED_HEADER_NONE_VALUE = "NONE";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The string prefix that the SSL termination service uses for its client
|
||||||
|
* verification header to represent that the client certificate has failed
|
||||||
|
* validation. The error message describing the nature of the failure is
|
||||||
|
* provided by the SSL termination service after this prefix.
|
||||||
|
*/
|
||||||
|
private static final String CLIENT_VERIFIED_HEADER_FAILED_PREFIX = "FAILED:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger for this class.
|
||||||
|
*/
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(SSLClientAuthenticationResource.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for retrieving configuration information.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private ConfigurationService confService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session manager for generating and maintaining unique tokens to
|
||||||
|
* represent the authentication flow of a user who has only partially
|
||||||
|
* authenticated. Here, these tokens represent a user that has been
|
||||||
|
* validated by SSL termination and allow the Guacamole instance that
|
||||||
|
* doesn't require SSL/TLS authentication to retrieve the user's identity
|
||||||
|
* and complete the authentication process.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private SSLAuthenticationSessionManager sessionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for validating and generating unique nonce values. Here, these
|
||||||
|
* nonces are used specifically for generating unique domains.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private NonceService subdomainNonceService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a single value from the HTTP header having the given name. If
|
||||||
|
* there are multiple HTTP headers present with this name, the first
|
||||||
|
* matching header in the request is used. If there are no such headers in
|
||||||
|
* the request, null is returned.
|
||||||
|
*
|
||||||
|
* @param headers
|
||||||
|
* The HTTP headers present in the request.
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* The name of the header to retrieve.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The first value of the HTTP header having the given name, or null if
|
||||||
|
* there is no such header.
|
||||||
|
*/
|
||||||
|
private String getHeader(HttpHeaders headers, String name) {
|
||||||
|
|
||||||
|
List<String> values = headers.getRequestHeader(name);
|
||||||
|
if (values.isEmpty())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return values.get(0);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the provided URL-encoded string as UTF-8, returning the result.
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* The URL-encoded string to decode.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The decoded string.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the provided value is not a valid URL-encoded string.
|
||||||
|
*/
|
||||||
|
private byte[] decode(String value) throws GuacamoleException {
|
||||||
|
try {
|
||||||
|
return URLDecoder.decode(value, StandardCharsets.UTF_8.name())
|
||||||
|
.getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException e) {
|
||||||
|
throw new GuacamoleClientException("Invalid URL-encoded value.", e);
|
||||||
|
}
|
||||||
|
catch (UnsupportedEncodingException e) {
|
||||||
|
// This should never happen, as UTF-8 is a standard charset that
|
||||||
|
// the JVM is required to support
|
||||||
|
throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a user's username from the X.509 subject name, which should be
|
||||||
|
* in LDAP DN format. If specific username attributes are configured, only
|
||||||
|
* those username attributes are used to determine the name. If a specific
|
||||||
|
* base DN is configured, only subject names that are formatted as LDAP DNs
|
||||||
|
* within that base DN will be accepted.
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* The subject name to extract the username from.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The username of the user represented by the given subject name.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If any configuration parameters related to retrieving certificates
|
||||||
|
* from HTTP request cannot be parsed, or if the provided subject name
|
||||||
|
* cannot be parsed or is not acceptable (wrong base DN or wrong
|
||||||
|
* username attribute).
|
||||||
|
*/
|
||||||
|
public String getUsername(String name) throws GuacamoleException {
|
||||||
|
|
||||||
|
// Extract user's DN from their X.509 certificate
|
||||||
|
LdapName dn;
|
||||||
|
try {
|
||||||
|
dn = new LdapName(name);
|
||||||
|
}
|
||||||
|
catch (InvalidNameException e) {
|
||||||
|
throw new GuacamoleClientException("Subject \"" + name + "\" is "
|
||||||
|
+ "not a valid DN: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify DN actually contains components
|
||||||
|
int numComponents = dn.size();
|
||||||
|
if (numComponents < 1)
|
||||||
|
throw new GuacamoleClientException("Subject DN is empty.");
|
||||||
|
|
||||||
|
// Verify DN is within configured base DN (if any)
|
||||||
|
LdapName baseDN = confService.getSubjectBaseDN();
|
||||||
|
if (baseDN != null && !(numComponents > baseDN.size() && dn.startsWith(baseDN)))
|
||||||
|
throw new GuacamoleClientException("Subject DN \"" + dn + "\" is "
|
||||||
|
+ "not within the configured base DN.");
|
||||||
|
|
||||||
|
// Retrieve the least significant attribute from the parsed DN - this
|
||||||
|
// will be the username
|
||||||
|
Rdn nameRdn = dn.getRdn(numComponents - 1);
|
||||||
|
|
||||||
|
// Verify that the username is specified with one of the allowed
|
||||||
|
// attributes
|
||||||
|
List<String> usernameAttributes = confService.getSubjectUsernameAttributes();
|
||||||
|
if (usernameAttributes != null && !usernameAttributes.stream().anyMatch(nameRdn.getType()::equalsIgnoreCase))
|
||||||
|
throw new GuacamoleClientException("Subject DN \"" + dn + "\" "
|
||||||
|
+ "does not contain an acceptable username attribute.");
|
||||||
|
|
||||||
|
// The DN is valid - extract the username from the least significant
|
||||||
|
// component
|
||||||
|
String username = nameRdn.getValue().toString();
|
||||||
|
logger.debug("Username \"{}\" extracted from subject DN \"{}\".", username, dn);
|
||||||
|
return username;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticates a user using HTTP headers containing that user's verified
|
||||||
|
* X.509 certificate. It is assumed that this certificate is being passed
|
||||||
|
* to Guacamole from an SSL termination service that has already verified
|
||||||
|
* that this certificate is valid and authorized for access to that
|
||||||
|
* Guacamole instance.
|
||||||
|
*
|
||||||
|
* @param certificate
|
||||||
|
* The raw bytes of the X.509 certificate retrieved from the request.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The username of the user asserted by the SSL termination service via
|
||||||
|
* that user's X.509 certificate.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If any configuration parameters related to retrieving certificates
|
||||||
|
* from HTTP request cannot be parsed, or if the certificate is not
|
||||||
|
* valid/present.
|
||||||
|
*/
|
||||||
|
public String getUsername(byte[] certificate) throws GuacamoleException {
|
||||||
|
|
||||||
|
// Parse and re-verify certificate is valid with respect to timestamps
|
||||||
|
X509CertificateHolder cert;
|
||||||
|
try (Reader reader = new StringReader(new String(certificate, StandardCharsets.UTF_8))) {
|
||||||
|
|
||||||
|
PEMParser parser = new PEMParser(reader);
|
||||||
|
cert = (X509CertificateHolder) parser.readObject();
|
||||||
|
|
||||||
|
// Verify certificate is valid (it should be given pre-validation
|
||||||
|
// from SSL termination, but it's worth rechecking for sanity)
|
||||||
|
if (!cert.isValidOn(new Date()))
|
||||||
|
throw new GuacamoleClientException("Certificate has expired.");
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new GuacamoleServerException("Certificate could not be read: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract user's DN from their X.509 certificate in LDAP (RFC 4919) format
|
||||||
|
X500Name subject = X500Name.getInstance(RFC4519Style.INSTANCE, cert.getSubject());
|
||||||
|
return getUsername(subject.toString());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the X.509 certificate in the given set of HTTP request
|
||||||
|
* headers, returning an authentication session token representing the
|
||||||
|
* identity in that certificate. If the certificate is invalid or not
|
||||||
|
* present, an invalid session token is returned.
|
||||||
|
*
|
||||||
|
* @param headers
|
||||||
|
* The headers of the HTTP request to process.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An authentication session token representing the identity in the
|
||||||
|
* certificate in the given HTTP request, or an invalid session token
|
||||||
|
* if no valid identity was asserted.
|
||||||
|
*/
|
||||||
|
private String processCertificate(HttpHeaders headers) {
|
||||||
|
|
||||||
|
//
|
||||||
|
// NOTE: A result with an associated state is ALWAYS returned by
|
||||||
|
// processCertificate(), even if the request does not actually contain
|
||||||
|
// a valid certificate. This is by design and ensures that the nature
|
||||||
|
// of a certificate (valid vs. invalid) cannot be determined except
|
||||||
|
// via Guacamole's authentication endpoint, thus allowing auth failure
|
||||||
|
// hooks to consider attempts to use invalid certificates as auth
|
||||||
|
// failures.
|
||||||
|
//
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Verify that SSL termination has already verified the certificate
|
||||||
|
String verified = getHeader(headers, confService.getClientVerifiedHeader());
|
||||||
|
if (verified != null && verified.startsWith(CLIENT_VERIFIED_HEADER_FAILED_PREFIX)) {
|
||||||
|
String message = verified.substring(CLIENT_VERIFIED_HEADER_FAILED_PREFIX.length());
|
||||||
|
throw new GuacamoleClientException("Client certificate did "
|
||||||
|
+ "not pass validation. SSL termination reports the "
|
||||||
|
+ "following failure: \"" + message + "\"");
|
||||||
|
}
|
||||||
|
else if (CLIENT_VERIFIED_HEADER_NONE_VALUE.equals(verified)) {
|
||||||
|
throw new GuacamoleClientException("No client certificate was presented.");
|
||||||
|
}
|
||||||
|
else if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified)) {
|
||||||
|
throw new GuacamoleClientException("Client certificate did not pass validation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String certificate = getHeader(headers, confService.getClientCertificateHeader());
|
||||||
|
if (certificate == null)
|
||||||
|
throw new GuacamoleClientException("Client certificate missing from request.");
|
||||||
|
|
||||||
|
String username = getUsername(decode(certificate));
|
||||||
|
long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity());
|
||||||
|
return sessionManager.defer(new SSLAuthenticationSession(username, validityDuration));
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (GuacamoleClientException e) {
|
||||||
|
logger.warn("SSL/TLS client authentication attempt rejected: {}", e.getMessage());
|
||||||
|
logger.debug("SSL/TLS client authentication failed.", e);
|
||||||
|
}
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
logger.error("SSL/TLS client authentication attempt could not be processed: {}", e.getMessage());
|
||||||
|
logger.debug("SSL/TLS client authentication failed.", e);
|
||||||
|
}
|
||||||
|
catch (RuntimeException | Error e) {
|
||||||
|
logger.error("SSL/TLS client authentication attempt failed internally: {}", e.getMessage());
|
||||||
|
logger.debug("Internal failure processing SSL/TLS client authentication attempt.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionManager.generateInvalid();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to authenticate the current user using SSL/TLS client
|
||||||
|
* authentication, returning an opaque value that represents their
|
||||||
|
* authenticated status. If necessary, the user is first redirected to a
|
||||||
|
* unique endpoint that supports SSL/TLS client authentication.
|
||||||
|
*
|
||||||
|
* @param headers
|
||||||
|
* All HTTP headers submitted in the user's authentication request.
|
||||||
|
*
|
||||||
|
* @param host
|
||||||
|
* The hostname that the user specified in their HTTP request.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A Response containing an opaque value representing the user's
|
||||||
|
* authenticated status, or a Response redirecting the user to a
|
||||||
|
* unique endpoint that can provide this.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If any required configuration information is missing or cannot be
|
||||||
|
* parsed, or if the request was not received at a valid subdomain.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("identity")
|
||||||
|
public Response authenticateClient(@Context HttpHeaders headers,
|
||||||
|
@HeaderParam("Host") String host) throws GuacamoleException {
|
||||||
|
|
||||||
|
// Redirect any requests to the domain that does NOT require SSL/TLS
|
||||||
|
// client authentication to the same endpoint at a domain that does
|
||||||
|
// require SSL/TLS authentication
|
||||||
|
String subdomain = confService.getClientAuthenticationSubdomain(host);
|
||||||
|
if (subdomain == null) {
|
||||||
|
|
||||||
|
long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxDomainValidity());
|
||||||
|
String uniqueSubdomain = subdomainNonceService.generate(validityDuration);
|
||||||
|
|
||||||
|
URI clientAuthURI = UriBuilder.fromUri(confService.getClientAuthenticationURI(uniqueSubdomain))
|
||||||
|
.path("api/ext/ssl/identity")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return Response.seeOther(clientAuthURI).build();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Process certificates only at valid single-use subdomains dedicated
|
||||||
|
// to client authentication, redirecting back to the main redirect URI
|
||||||
|
// for final authentication if that processing is successful.
|
||||||
|
//
|
||||||
|
// NOTE: This is CRITICAL. If unique subdomains are not generated and
|
||||||
|
// tied to strictly one authentication attempt, then those subdomains
|
||||||
|
// could be reused by a user on a shared machine to assume the cached
|
||||||
|
// credentials of another user that used that machine earlier. The
|
||||||
|
// browser and/or OS may cache the certificate so that it can be reused
|
||||||
|
// for future SSL sessions to that same domain. Here, we ensure each
|
||||||
|
// generated domain is unique and only valid for certificate processing
|
||||||
|
// ONCE. The domain may still be valid with DNS, but will no longer be
|
||||||
|
// usable for certificate authentication.
|
||||||
|
//
|
||||||
|
|
||||||
|
if (subdomainNonceService.isValid(subdomain))
|
||||||
|
return Response.ok(new OpaqueAuthenticationResult(processCertificate(headers)))
|
||||||
|
.header("Access-Control-Allow-Origin", confService.getPrimaryOrigin().toString())
|
||||||
|
.type(MediaType.APPLICATION_JSON)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
throw new GuacamoleResourceNotFoundException("No such resource.");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,440 @@
|
|||||||
|
/*
|
||||||
|
* 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.ssl.conf;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.naming.ldap.LdapName;
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.GuacamoleServerException;
|
||||||
|
import org.apache.guacamole.environment.Environment;
|
||||||
|
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
|
||||||
|
import org.apache.guacamole.properties.StringGuacamoleProperty;
|
||||||
|
import org.apache.guacamole.properties.StringListProperty;
|
||||||
|
import org.apache.guacamole.properties.URIGuacamoleProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for retrieving configuration information regarding SSO using SSL/TLS
|
||||||
|
* authentication.
|
||||||
|
*/
|
||||||
|
public class ConfigurationService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default name of the header to use to retrieve the URL-encoded client
|
||||||
|
* certificate from an HTTP request received from an SSL termination
|
||||||
|
* service providing SSL/TLS client authentication.
|
||||||
|
*/
|
||||||
|
private static String DEFAULT_CLIENT_CERTIFICATE_HEADER = "X-Client-Certificate";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default name of the header to use to retrieve the verification
|
||||||
|
* status of the certificate an HTTP request received from an SSL
|
||||||
|
* termination service providing SSL/TLS client authentication.
|
||||||
|
*/
|
||||||
|
private static String DEFAULT_CLIENT_VERIFIED_HEADER = "X-Client-Verified";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default amount of time that a temporary authentication token for
|
||||||
|
* SSL/TLS authentication may remain valid, in minutes.
|
||||||
|
*/
|
||||||
|
private static int DEFAULT_MAX_TOKEN_VALIDITY = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default amount of time that the temporary, unique subdomain
|
||||||
|
* generated for SSL/TLS authentication may remain valid, in minutes.
|
||||||
|
*/
|
||||||
|
private static int DEFAULT_MAX_DOMAIN_VALIDITY = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The property representing the URI that should be used to authenticate
|
||||||
|
* users with SSL/TLS client authentication. This must be a URI that points
|
||||||
|
* to THIS instance of Guacamole, but behind SSL termination that requires
|
||||||
|
* SSL/TLS client authentication.
|
||||||
|
*/
|
||||||
|
private static final WildcardURIGuacamoleProperty SSL_CLIENT_AUTH_URI =
|
||||||
|
new WildcardURIGuacamoleProperty() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "ssl-client-auth-uri"; }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The property representing the URI of this instance without SSL/TLS
|
||||||
|
* client authentication required. This must be a URI that points
|
||||||
|
* to THIS instance of Guacamole, but behind SSL termination that DOES NOT
|
||||||
|
* require or request SSL/TLS client authentication.
|
||||||
|
*/
|
||||||
|
private static final URIGuacamoleProperty SSL_PRIMARY_URI =
|
||||||
|
new URIGuacamoleProperty() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "ssl-primary-uri"; }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The property representing the name of the header to use to retrieve the
|
||||||
|
* URL-encoded client certificate from an HTTP request received from an
|
||||||
|
* SSL termination service providing SSL/TLS client authentication.
|
||||||
|
*/
|
||||||
|
private static final StringGuacamoleProperty SSL_CLIENT_CERTIFICATE_HEADER =
|
||||||
|
new StringGuacamoleProperty() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "ssl-client-certificate-header"; }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The property representing the name of the header to use to retrieve the
|
||||||
|
* verification status of the certificate an HTTP request received from an
|
||||||
|
* SSL termination service providing SSL/TLS client authentication. This
|
||||||
|
* value of this header must be "SUCCESS" (all uppercase) if the
|
||||||
|
* certificate was successfully verified.
|
||||||
|
*/
|
||||||
|
private static final StringGuacamoleProperty SSL_CLIENT_VERIFIED_HEADER =
|
||||||
|
new StringGuacamoleProperty() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "ssl-client-verified-header"; }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The property representing the amount of time that a temporary
|
||||||
|
* authentication token for SSL/TLS authentication may remain valid, in
|
||||||
|
* minutes. This token is used to represent the user's asserted identity
|
||||||
|
* after it has been verified by the SSL termination service. This interval
|
||||||
|
* must be long enough to allow for network delays in receiving the token,
|
||||||
|
* but short enough that unused tokens do not consume unnecessary server
|
||||||
|
* resources and cannot potentially be guessed while the token is still
|
||||||
|
* valid. These tokens are 256-bit secure random values.
|
||||||
|
*/
|
||||||
|
private static final IntegerGuacamoleProperty SSL_MAX_TOKEN_VALIDITY =
|
||||||
|
new IntegerGuacamoleProperty() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "ssl-max-token-validity"; }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The property defining the LDAP attribute or attributes that may be used
|
||||||
|
* to represent a username within the subject DN of a user's X.509
|
||||||
|
* certificate. If the least-significant attribute of the subject DN is not
|
||||||
|
* one of these attributes, the certificate will be rejected. By default,
|
||||||
|
* any attribute is accepted.
|
||||||
|
*/
|
||||||
|
private static final StringListProperty SSL_SUBJECT_USERNAME_ATTRIBUTE =
|
||||||
|
new StringListProperty () {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "ssl-subject-username-attribute"; }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The property defining the base DN containing all valid subject DNs. If
|
||||||
|
* specified, only certificates asserting subject DNs beneath this base DN
|
||||||
|
* will be accepted. By default, all DNs are accepted.
|
||||||
|
*/
|
||||||
|
private static final LdapNameGuacamoleProperty SSL_SUBJECT_BASE_DN =
|
||||||
|
new LdapNameGuacamoleProperty () {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "ssl-subject-base-dn"; }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The property representing the amount of time that the temporary, unique
|
||||||
|
* subdomain generated for SSL/TLS authentication may remain valid, in
|
||||||
|
* minutes. This subdomain is used to ensure each SSL/TLS authentication
|
||||||
|
* attempt is fresh and does not potentially reuse a previous
|
||||||
|
* authentication attempt that was cached by the browser or OS. This
|
||||||
|
* interval must be long enough to allow for network delays in
|
||||||
|
* authenticating the user with the SSL termination service that enforces
|
||||||
|
* SSL/TLS client authentication, but short enough that an unused domain
|
||||||
|
* does not consume unnecessary server resources and cannot potentially be
|
||||||
|
* guessed while that subdomain is still valid. These subdomains are
|
||||||
|
* 128-bit secure random values.
|
||||||
|
*/
|
||||||
|
private static final IntegerGuacamoleProperty SSL_MAX_DOMAIN_VALIDITY =
|
||||||
|
new IntegerGuacamoleProperty() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "ssl-max-domain-validity"; }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Guacamole server environment.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private Environment environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a URI that should be used to authenticate users with SSL/TLS
|
||||||
|
* client authentication. The returned URI will consist of the configured
|
||||||
|
* client authentication URI with the wildcard portion ("*.") replaced with
|
||||||
|
* the given subdomain.
|
||||||
|
*
|
||||||
|
* @param subdomain
|
||||||
|
* The subdomain that should replace the wildcard portion of the
|
||||||
|
* configured client authentication URI.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A URI that should be used to authenticate users with SSL/TLS
|
||||||
|
* client authentication.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the required property for configuring the client authentication
|
||||||
|
* URI is missing or cannot be parsed.
|
||||||
|
*/
|
||||||
|
public URI getClientAuthenticationURI(String subdomain) throws GuacamoleException {
|
||||||
|
|
||||||
|
URI authURI = environment.getRequiredProperty(SSL_CLIENT_AUTH_URI);
|
||||||
|
String baseHostname = authURI.getHost();
|
||||||
|
|
||||||
|
// Add provided subdomain to auth URI
|
||||||
|
return UriBuilder.fromUri(authURI)
|
||||||
|
.host(subdomain + "." + baseHostname)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a hostname that was used by a user for SSL/TLS client
|
||||||
|
* authentication, returns the subdomain at the beginning of that hostname.
|
||||||
|
* If the hostname does not match the pattern of hosts represented by the
|
||||||
|
* configured client authentication URI, null is returned.
|
||||||
|
*
|
||||||
|
* @param hostname
|
||||||
|
* The hostname to extract the subdomain from.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The subdomain at the beginning of the provided hostname, if that
|
||||||
|
* hostname matches the pattern of hosts represented by the
|
||||||
|
* configured client authentication URI, or null otherwise.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the required property for configuring the client authentication
|
||||||
|
* URI is missing or cannot be parsed.
|
||||||
|
*/
|
||||||
|
public String getClientAuthenticationSubdomain(String hostname) throws GuacamoleException {
|
||||||
|
|
||||||
|
// Any hostname that matches the explicitly-specific primary URI is not
|
||||||
|
// a client auth subdomain
|
||||||
|
if (isPrimaryHostname(hostname))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
URI authURI = environment.getRequiredProperty(SSL_CLIENT_AUTH_URI);
|
||||||
|
String baseHostname = authURI.getHost();
|
||||||
|
|
||||||
|
// Verify the first domain component is at least one character in
|
||||||
|
// length
|
||||||
|
int firstPeriod = hostname.indexOf('.');
|
||||||
|
if (firstPeriod <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Verify domain matches the configured auth URI except for the leading
|
||||||
|
// subdomain
|
||||||
|
if (!hostname.regionMatches(true, firstPeriod + 1, baseHostname, 0, baseHostname.length()))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Extract subdomain
|
||||||
|
return hostname.substring(0, firstPeriod);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the URI of this instance without SSL/TLS client authentication
|
||||||
|
* required.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The URI of this instance without SSL/TLS client authentication
|
||||||
|
* required.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the required property for configuring the primary URI is missing
|
||||||
|
* or cannot be parsed.
|
||||||
|
*/
|
||||||
|
public URI getPrimaryURI() throws GuacamoleException {
|
||||||
|
return environment.getRequiredProperty(SSL_PRIMARY_URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the HTTP request origin for requests originating from this
|
||||||
|
* instance via the primary URI (as returned by {@link #getPrimaryURI()}.
|
||||||
|
* This value is essentially the same as the primary URI but with only the
|
||||||
|
* scheme, host, and port present.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The HTTP request origin for requests originating from this instance
|
||||||
|
* via the primary URI.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the required property for configuring the primary URI is missing
|
||||||
|
* or cannot be parsed.
|
||||||
|
*/
|
||||||
|
public URI getPrimaryOrigin() throws GuacamoleException {
|
||||||
|
URI primaryURI = getPrimaryURI();
|
||||||
|
try {
|
||||||
|
return new URI(primaryURI.getScheme(), null, primaryURI.getHost(), primaryURI.getPort(), null, null, null);
|
||||||
|
}
|
||||||
|
catch (URISyntaxException e) {
|
||||||
|
throw new GuacamoleServerException("Request origin could not be "
|
||||||
|
+ "derived from the configured primary URI.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the given hostname is the same as the hostname in the
|
||||||
|
* primary URI (as returned by {@link #getPrimaryURI()}. Hostnames are
|
||||||
|
* case-insensitive.
|
||||||
|
*
|
||||||
|
* @param hostname
|
||||||
|
* The hostname to test.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* true if the hostname is the same as the hostname in the primary URI,
|
||||||
|
* false otherwise.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the required property for configuring the primary URI is missing
|
||||||
|
* or cannot be parsed.
|
||||||
|
*/
|
||||||
|
public boolean isPrimaryHostname(String hostname) throws GuacamoleException {
|
||||||
|
URI primaryURI = getPrimaryURI();
|
||||||
|
return hostname.equalsIgnoreCase(primaryURI.getHost());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the header to use to retrieve the URL-encoded client
|
||||||
|
* certificate from an HTTP request received from an SSL termination
|
||||||
|
* service providing SSL/TLS client authentication.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The name of the header to use to retrieve the URL-encoded client
|
||||||
|
* certificate from an HTTP request received from an SSL termination
|
||||||
|
* service providing SSL/TLS client authentication.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the property for configuring the client certificate header cannot
|
||||||
|
* be parsed.
|
||||||
|
*/
|
||||||
|
public String getClientCertificateHeader() throws GuacamoleException {
|
||||||
|
return environment.getProperty(SSL_CLIENT_CERTIFICATE_HEADER, DEFAULT_CLIENT_CERTIFICATE_HEADER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the header to use to retrieve the verification
|
||||||
|
* status of the certificate an HTTP request received from an SSL
|
||||||
|
* termination service providing SSL/TLS client authentication.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The name of the header to use to retrieve the verification
|
||||||
|
* status of the certificate an HTTP request received from an SSL
|
||||||
|
* termination service providing SSL/TLS client authentication.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the property for configuring the client verification header
|
||||||
|
* cannot be parsed.
|
||||||
|
*/
|
||||||
|
public String getClientVerifiedHeader() throws GuacamoleException {
|
||||||
|
return environment.getProperty(SSL_CLIENT_VERIFIED_HEADER, DEFAULT_CLIENT_VERIFIED_HEADER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum amount of time that the token generated by the
|
||||||
|
* Guacamole server representing current SSL authentication state should
|
||||||
|
* remain valid, in minutes. This imposes an upper limit on the amount of
|
||||||
|
* time any particular authentication request can result in successful
|
||||||
|
* authentication within Guacamole when SSL/TLS client authentication is
|
||||||
|
* configured. By default, this will be 5.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The maximum amount of time that an SSL authentication token
|
||||||
|
* generated by the Guacamole server should remain valid, in minutes.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If guacamole.properties cannot be parsed.
|
||||||
|
*/
|
||||||
|
public int getMaxTokenValidity() throws GuacamoleException {
|
||||||
|
return environment.getProperty(SSL_MAX_TOKEN_VALIDITY, DEFAULT_MAX_TOKEN_VALIDITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum amount of time that a unique client authentication
|
||||||
|
* subdomain generated by the Guacamole server should remain valid, in
|
||||||
|
* minutes. This imposes an upper limit on the amount of time any
|
||||||
|
* particular authentication request can result in successful
|
||||||
|
* authentication within Guacamole when SSL/TLS client authentication is
|
||||||
|
* configured. By default, this will be 5.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The maximum amount of time that a unique client authentication
|
||||||
|
* subdomain generated by the Guacamole server should remain valid, in
|
||||||
|
* minutes.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If guacamole.properties cannot be parsed.
|
||||||
|
*/
|
||||||
|
public int getMaxDomainValidity() throws GuacamoleException {
|
||||||
|
return environment.getProperty(SSL_MAX_DOMAIN_VALIDITY, DEFAULT_MAX_DOMAIN_VALIDITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the base DN that contains all valid subject DNs. If there is no
|
||||||
|
* such base DN (and all subject DNs are valid), null is returned.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The base DN that contains all valid subject DNs, or null if all
|
||||||
|
* subject DNs are valid.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the configured base DN cannot be read or is not a valid LDAP DN.
|
||||||
|
*/
|
||||||
|
public LdapName getSubjectBaseDN() throws GuacamoleException {
|
||||||
|
return environment.getProperty(SSL_SUBJECT_BASE_DN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all attributes that may be used to represent a user's
|
||||||
|
* username within their subject DN. If all attributes may be accepted,
|
||||||
|
* null is returned.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A list of all attributes that may be used to represent a user's
|
||||||
|
* username within their subject DN, or null if any attribute may be
|
||||||
|
* used.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the configured set of username attributes cannot be read.
|
||||||
|
*/
|
||||||
|
public List<String> getSubjectUsernameAttributes() throws GuacamoleException {
|
||||||
|
return environment.getProperty(SSL_SUBJECT_USERNAME_ATTRIBUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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.ssl.conf;
|
||||||
|
|
||||||
|
import javax.naming.InvalidNameException;
|
||||||
|
import javax.naming.ldap.LdapName;
|
||||||
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.GuacamoleServerException;
|
||||||
|
import org.apache.guacamole.properties.GuacamoleProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A GuacamoleProperty whose value is an LDAP name, such as a distinguished
|
||||||
|
* name.
|
||||||
|
*/
|
||||||
|
public abstract class LdapNameGuacamoleProperty implements GuacamoleProperty<LdapName> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LdapName parseValue(String value) throws GuacamoleException {
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new LdapName(value);
|
||||||
|
}
|
||||||
|
catch (InvalidNameException e) {
|
||||||
|
throw new GuacamoleServerException("Value \"" + value
|
||||||
|
+ "\" is not a valid LDAP name.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* 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.ssl.conf;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.GuacamoleServerException;
|
||||||
|
import org.apache.guacamole.properties.URIGuacamoleProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A GuacamoleProperty whose value is a wildcard URI. The behavior of this
|
||||||
|
* property is identical to URIGuacamoleProperty except that it verifies a
|
||||||
|
* wildcard hostname prefix ("*.") is present and strips that prefix from the
|
||||||
|
* parsed URI.
|
||||||
|
*/
|
||||||
|
public abstract class WildcardURIGuacamoleProperty extends URIGuacamoleProperty {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expression that broadly matches URIs that contain wildcards in
|
||||||
|
* their hostname. This regular expression is NOT strict and will match
|
||||||
|
* invalid URIs. It is only strict enough to recognize a wildcard hostname
|
||||||
|
* prefix.
|
||||||
|
*/
|
||||||
|
private static final Pattern WILDCARD_URI_PATTERN = Pattern.compile("([^:]+://(?:[^@]+@)?)\\*\\.(.*)");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URI parseValue(String value) throws GuacamoleException {
|
||||||
|
|
||||||
|
// Verify wildcard prefix is present
|
||||||
|
Matcher matcher = WILDCARD_URI_PATTERN.matcher(value);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
|
||||||
|
// Strip wildcard prefix from URI and verify a valid hostname is
|
||||||
|
// still present
|
||||||
|
URI uri = super.parseValue(matcher.group(1) + matcher.group(2));
|
||||||
|
if (uri.getHost() != null)
|
||||||
|
return uri;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other values are not valid wildcard URIs
|
||||||
|
throw new GuacamoleServerException("Value \"" + value
|
||||||
|
+ "\" is not a valid wildcard URI.");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A directive which automatically attempts to log the current user in using
|
||||||
|
* SSL/TLS client authentication when the associated element is clicked.
|
||||||
|
*/
|
||||||
|
angular.module('element').directive('guacSslAuth', ['$injector', function guacSslAuth($injector) {
|
||||||
|
|
||||||
|
// Required services
|
||||||
|
var clientAuthService = $injector.get('clientAuthService');
|
||||||
|
|
||||||
|
var directive = {
|
||||||
|
restrict: 'A'
|
||||||
|
};
|
||||||
|
|
||||||
|
directive.link = function linkGuacSslAuth($scope, $element) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The element which will register the click.
|
||||||
|
*
|
||||||
|
* @type Element
|
||||||
|
*/
|
||||||
|
const element = $element[0];
|
||||||
|
|
||||||
|
// Attempt SSL/TLS client authentication upon click
|
||||||
|
element.addEventListener('click', function elementClicked() {
|
||||||
|
clientAuthService.authenticate();
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return directive;
|
||||||
|
|
||||||
|
}]);
|
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
|
||||||
|
"guacamoleVersion" : "1.5.0",
|
||||||
|
|
||||||
|
"name" : "SSL Authentication Extension",
|
||||||
|
"namespace" : "ssl",
|
||||||
|
|
||||||
|
"authProviders" : [
|
||||||
|
"org.apache.guacamole.auth.ssl.SSLAuthenticationProvider"
|
||||||
|
],
|
||||||
|
|
||||||
|
"css" : [
|
||||||
|
"styles/sso-providers.css"
|
||||||
|
],
|
||||||
|
|
||||||
|
"js" : [ "ssl.min.js" ],
|
||||||
|
|
||||||
|
"html" : [
|
||||||
|
"html/sso-providers.html",
|
||||||
|
"html/sso-provider-ssl.html"
|
||||||
|
],
|
||||||
|
|
||||||
|
"translations" : [
|
||||||
|
"translations/ca.json",
|
||||||
|
"translations/de.json",
|
||||||
|
"translations/en.json",
|
||||||
|
"translations/fr.json",
|
||||||
|
"translations/ja.json",
|
||||||
|
"translations/ko.json",
|
||||||
|
"translations/pt.json",
|
||||||
|
"translations/ru.json",
|
||||||
|
"translations/zh.json"
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,4 @@
|
|||||||
|
<meta name="after-children" content=".login-ui .sso-provider-list:last-child">
|
||||||
|
<li class="sso-provider sso-provider-ssl"><a guac-ssl-auth href="">{{
|
||||||
|
'LOGIN.NAME_IDP_SSL' | translate
|
||||||
|
}}</a></li>
|
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for authenticating a user using SSL/TLS client authentication.
|
||||||
|
*/
|
||||||
|
angular.module('guacSsoSsl').factory('clientAuthService', ['$injector',
|
||||||
|
function clientAuthServiceProvider($injector) {
|
||||||
|
|
||||||
|
// Required services
|
||||||
|
var requestService = $injector.get('requestService');
|
||||||
|
var authenticationService = $injector.get('authenticationService');
|
||||||
|
|
||||||
|
var service = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to authenticate using a unique token obtained through SSL/TLS
|
||||||
|
* client authentication.
|
||||||
|
*/
|
||||||
|
service.authenticate = function authenticate() {
|
||||||
|
|
||||||
|
// Transform SSL/TLS identity into an opaque "state" value and
|
||||||
|
// attempt authentication using that value
|
||||||
|
authenticationService.authenticate(
|
||||||
|
requestService({
|
||||||
|
method: 'GET',
|
||||||
|
headers : {
|
||||||
|
'Cache-Control' : undefined, // Avoid sending headers that would result in a pre-flight OPTIONS request for CORS
|
||||||
|
'Pragma' : undefined
|
||||||
|
},
|
||||||
|
url: 'api/ext/ssl/identity'
|
||||||
|
})
|
||||||
|
.then(function identityRetrieved(data) {
|
||||||
|
return { 'state' : data.state || '' };
|
||||||
|
})
|
||||||
|
)['catch'](requestService.IGNORE);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return service;
|
||||||
|
|
||||||
|
}]);
|
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The module for code implementing SSO using SSL/TLS client authentication.
|
||||||
|
*/
|
||||||
|
angular.module('guacSsoSsl', [
|
||||||
|
'auth',
|
||||||
|
'rest'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Ensure the guacSsoSsl module is loaded along with the rest of the app
|
||||||
|
angular.module('index').requires.push('guacSsoSsl');
|
@@ -49,6 +49,7 @@
|
|||||||
<module>modules/guacamole-auth-sso-cas</module>
|
<module>modules/guacamole-auth-sso-cas</module>
|
||||||
<module>modules/guacamole-auth-sso-openid</module>
|
<module>modules/guacamole-auth-sso-openid</module>
|
||||||
<module>modules/guacamole-auth-sso-saml</module>
|
<module>modules/guacamole-auth-sso-saml</module>
|
||||||
|
<module>modules/guacamole-auth-sso-ssl</module>
|
||||||
|
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
|
@@ -18,25 +18,47 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service for authenticating a user against the REST API.
|
* A service for authenticating a user against the REST API. Invoking the
|
||||||
|
* authenticate() or login() functions of this service will automatically
|
||||||
|
* affect the login dialog, if visible.
|
||||||
*
|
*
|
||||||
* This service broadcasts two events on $rootScope depending on the result of
|
* This service broadcasts events on $rootScope depending on the status and
|
||||||
* authentication operations: 'guacLogin' if authentication was successful and
|
* result of authentication operations:
|
||||||
* a new token was created, and 'guacLogout' if an existing token is being
|
|
||||||
* destroyed or replaced. Both events will be passed the related token as their
|
|
||||||
* sole parameter.
|
|
||||||
*
|
*
|
||||||
* If a login attempt results in an existing token being replaced, 'guacLogout'
|
* "guacLoginPending"
|
||||||
* will be broadcast first for the token being replaced, followed by
|
* An authentication request is being submitted and we are awaiting the
|
||||||
* 'guacLogin' for the new token.
|
* result. The request may not yet have been submitted if the parameters
|
||||||
|
* for that request are not ready. This event receives a promise that
|
||||||
|
* resolves with the HTTP parameters that were ultimately submitted as its
|
||||||
|
* sole parameter.
|
||||||
*
|
*
|
||||||
* Failed logins may also result in guacInsufficientCredentials or
|
* "guacLogin"
|
||||||
* guacInvalidCredentials events, if the provided credentials were rejected for
|
* Authentication was successful and a new token was created. This event
|
||||||
* being insufficient or invalid respectively. Both events will be provided
|
* receives the authentication token as its sole parameter.
|
||||||
* the set of parameters originally given to authenticate() and the error that
|
*
|
||||||
* rejected the credentials. The Error object provided will contain set of
|
* "guacLogout"
|
||||||
* expected credentials returned by the REST endpoint. This set of credentials
|
* An existing token is being destroyed. This event receives the
|
||||||
* will be in the form of a Field array.
|
* authentication token as its sole parameter. If the existing token for
|
||||||
|
* the current session is being replaced without destroying that session,
|
||||||
|
* this event is not fired.
|
||||||
|
*
|
||||||
|
* "guacLoginFailed"
|
||||||
|
* An authentication request has failed for any reason. This event is
|
||||||
|
* broadcast before any other events that are specific to the nature of
|
||||||
|
* the failure, and may be used to detect login failures in lieu of those
|
||||||
|
* events. This event receives two parameters: the HTTP parameters
|
||||||
|
* submitted and the Error object received from the REST endpoint.
|
||||||
|
*
|
||||||
|
* "guacInsufficientCredentials"
|
||||||
|
* An authentication request failed because additional credentials are
|
||||||
|
* needed before the request can be processed. This event receives two
|
||||||
|
* parameters: the HTTP parameters submitted and the Error object received
|
||||||
|
* from the REST endpoint.
|
||||||
|
*
|
||||||
|
* "guacInvalidCredentials"
|
||||||
|
* An authentication request failed because the credentials provided are
|
||||||
|
* invalid. This event receives two parameters: the HTTP parameters
|
||||||
|
* submitted and the Error object received from the REST endpoint.
|
||||||
*/
|
*/
|
||||||
angular.module('auth').factory('authenticationService', ['$injector',
|
angular.module('auth').factory('authenticationService', ['$injector',
|
||||||
function authenticationService($injector) {
|
function authenticationService($injector) {
|
||||||
@@ -46,6 +68,7 @@ angular.module('auth').factory('authenticationService', ['$injector',
|
|||||||
var Error = $injector.get('Error');
|
var Error = $injector.get('Error');
|
||||||
|
|
||||||
// Required services
|
// Required services
|
||||||
|
var $q = $injector.get('$q');
|
||||||
var $rootScope = $injector.get('$rootScope');
|
var $rootScope = $injector.get('$rootScope');
|
||||||
var localStorageService = $injector.get('localStorageService');
|
var localStorageService = $injector.get('localStorageService');
|
||||||
var requestService = $injector.get('requestService');
|
var requestService = $injector.get('requestService');
|
||||||
@@ -141,7 +164,8 @@ angular.module('auth').factory('authenticationService', ['$injector',
|
|||||||
* and given arbitrary parameters, returning a promise that succeeds only
|
* and given arbitrary parameters, returning a promise that succeeds only
|
||||||
* if the authentication operation was successful. The resulting
|
* if the authentication operation was successful. The resulting
|
||||||
* authentication data can be retrieved later via getCurrentToken() or
|
* authentication data can be retrieved later via getCurrentToken() or
|
||||||
* getCurrentUsername().
|
* getCurrentUsername(). Invoking this function will affect the UI,
|
||||||
|
* including the login screen if visible.
|
||||||
*
|
*
|
||||||
* The provided parameters can be virtually any object, as each property
|
* The provided parameters can be virtually any object, as each property
|
||||||
* will be sent as an HTTP parameter in the authentication request.
|
* will be sent as an HTTP parameter in the authentication request.
|
||||||
@@ -151,57 +175,75 @@ angular.module('auth').factory('authenticationService', ['$injector',
|
|||||||
*
|
*
|
||||||
* If a token is provided, it will be reused if possible.
|
* If a token is provided, it will be reused if possible.
|
||||||
*
|
*
|
||||||
* @param {Object} parameters
|
* @param {Object|Promise} parameters
|
||||||
* Arbitrary parameters to authenticate with.
|
* Arbitrary parameters to authenticate with. If a Promise is provided,
|
||||||
|
* that Promise must resolve with the parameters to be submitted when
|
||||||
|
* those parameters are available, and any error will be handled as if
|
||||||
|
* from the authentication endpoint of the REST API itself.
|
||||||
*
|
*
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
* A promise which succeeds only if the login operation was successful.
|
* A promise which succeeds only if the login operation was successful.
|
||||||
*/
|
*/
|
||||||
service.authenticate = function authenticate(parameters) {
|
service.authenticate = function authenticate(parameters) {
|
||||||
|
|
||||||
// Attempt authentication
|
// Coerce received parameters object into a Promise, if it isn't
|
||||||
return requestService({
|
// already a Promise
|
||||||
method: 'POST',
|
parameters = $q.resolve(parameters);
|
||||||
url: 'api/tokens',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
|
||||||
},
|
|
||||||
data: $.param(parameters)
|
|
||||||
})
|
|
||||||
|
|
||||||
// If authentication succeeds, handle received auth data
|
// Notify that a fresh authentication request is underway
|
||||||
.then(function authenticationSuccessful(data) {
|
$rootScope.$broadcast('guacLoginPending', parameters);
|
||||||
|
|
||||||
var currentToken = service.getCurrentToken();
|
// Attempt authentication after auth parameters are available ...
|
||||||
|
return parameters.then(function requestParametersReady(requestParams) {
|
||||||
|
|
||||||
// If a new token was received, ensure the old token is invalidated,
|
return requestService({
|
||||||
// if any, and notify listeners of the new token
|
method: 'POST',
|
||||||
if (data.authToken !== currentToken) {
|
url: 'api/tokens',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
data: $.param(requestParams)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ... if authentication succeeds, handle received auth data ...
|
||||||
|
.then(function authenticationSuccessful(data) {
|
||||||
|
|
||||||
|
var currentToken = service.getCurrentToken();
|
||||||
|
|
||||||
|
// If a new token was received, ensure the old token is invalidated,
|
||||||
|
// if any, and notify listeners of the new token
|
||||||
|
if (data.authToken !== currentToken) {
|
||||||
|
|
||||||
|
// If an old token existed, request that the token be revoked
|
||||||
|
if (currentToken) {
|
||||||
|
service.revokeToken(currentToken).catch(angular.noop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify of login and new token
|
||||||
|
setAuthenticationResult(new AuthenticationResult(data));
|
||||||
|
$rootScope.$broadcast('guacLogin', data.authToken);
|
||||||
|
|
||||||
// If an old token existed, request that the token be revoked
|
|
||||||
if (currentToken) {
|
|
||||||
service.revokeToken(currentToken).catch(angular.noop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify of login and new token
|
// Update cached authentication result, even if the token remains
|
||||||
setAuthenticationResult(new AuthenticationResult(data));
|
// the same
|
||||||
$rootScope.$broadcast('guacLogin', data.authToken);
|
else
|
||||||
|
setAuthenticationResult(new AuthenticationResult(data));
|
||||||
|
|
||||||
}
|
// Authentication was successful
|
||||||
|
return data;
|
||||||
|
|
||||||
// Update cached authentication result, even if the token remains
|
});
|
||||||
// the same
|
|
||||||
else
|
|
||||||
setAuthenticationResult(new AuthenticationResult(data));
|
|
||||||
|
|
||||||
// Authentication was successful
|
|
||||||
return data;
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// If authentication fails, propogate failure to returned promise
|
// ... if authentication fails, propogate failure to returned promise
|
||||||
['catch'](requestService.createErrorCallback(function authenticationFailed(error) {
|
['catch'](requestService.createErrorCallback(function authenticationFailed(error) {
|
||||||
|
|
||||||
|
// Notify of generic login failure, for any event consumers that
|
||||||
|
// wish to handle all types of failures at once
|
||||||
|
$rootScope.$broadcast('guacLoginFailed', parameters, error);
|
||||||
|
|
||||||
// Request credentials if provided credentials were invalid
|
// Request credentials if provided credentials were invalid
|
||||||
if (error.type === Error.Type.INVALID_CREDENTIALS) {
|
if (error.type === Error.Type.INVALID_CREDENTIALS) {
|
||||||
$rootScope.$broadcast('guacInvalidCredentials', parameters, error);
|
$rootScope.$broadcast('guacInvalidCredentials', parameters, error);
|
||||||
@@ -321,7 +363,8 @@ angular.module('auth').factory('authenticationService', ['$injector',
|
|||||||
* with a username and password, ignoring any currently-stored token,
|
* with a username and password, ignoring any currently-stored token,
|
||||||
* returning a promise that succeeds only if the login operation was
|
* returning a promise that succeeds only if the login operation was
|
||||||
* successful. The resulting authentication data can be retrieved later
|
* successful. The resulting authentication data can be retrieved later
|
||||||
* via getCurrentToken() or getCurrentUsername().
|
* via getCurrentToken() or getCurrentUsername(). Invoking this function
|
||||||
|
* will affect the UI, including the login screen if visible.
|
||||||
*
|
*
|
||||||
* @param {String} username
|
* @param {String} username
|
||||||
* The username to log in with.
|
* The username to log in with.
|
||||||
@@ -342,7 +385,9 @@ angular.module('auth').factory('authenticationService', ['$injector',
|
|||||||
/**
|
/**
|
||||||
* Makes a request to logout a user using the token REST API endpoint,
|
* Makes a request to logout a user using the token REST API endpoint,
|
||||||
* returning a promise that succeeds only if the logout operation was
|
* returning a promise that succeeds only if the logout operation was
|
||||||
* successful.
|
* successful. Invoking this function will affect the UI, causing the
|
||||||
|
* visible components of the application to be replaced with a status
|
||||||
|
* message noting that the user has been logged out.
|
||||||
*
|
*
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
* A promise which succeeds only if the logout operation was
|
* A promise which succeeds only if the logout operation was
|
||||||
|
@@ -177,51 +177,7 @@ angular.module('login').directive('guacLogin', [function guacLogin() {
|
|||||||
* authentication service, redirecting to the main view if successful.
|
* authentication service, redirecting to the main view if successful.
|
||||||
*/
|
*/
|
||||||
$scope.login = function login() {
|
$scope.login = function login() {
|
||||||
|
authenticationService.authenticate($scope.enteredValues)['catch'](requestService.IGNORE);
|
||||||
// Authentication is now in progress
|
|
||||||
$scope.submitted = true;
|
|
||||||
|
|
||||||
// Start with cleared status
|
|
||||||
$scope.loginError = null;
|
|
||||||
|
|
||||||
// Attempt login once existing session is destroyed
|
|
||||||
authenticationService.authenticate($scope.enteredValues)
|
|
||||||
|
|
||||||
// Retry route upon success (entered values will be cleared only
|
|
||||||
// after route change has succeeded as this can take time)
|
|
||||||
.then(function loginSuccessful() {
|
|
||||||
$route.reload();
|
|
||||||
})
|
|
||||||
|
|
||||||
// Reset upon failure
|
|
||||||
['catch'](requestService.createErrorCallback(function loginFailed(error) {
|
|
||||||
|
|
||||||
// Initial submission is complete and has failed
|
|
||||||
$scope.submitted = false;
|
|
||||||
|
|
||||||
// Clear out passwords if the credentials were rejected for any reason
|
|
||||||
if (error.type !== Error.Type.INSUFFICIENT_CREDENTIALS) {
|
|
||||||
|
|
||||||
// Flag generic error for invalid login
|
|
||||||
if (error.type === Error.Type.INVALID_CREDENTIALS)
|
|
||||||
$scope.loginError = {
|
|
||||||
'key' : 'LOGIN.ERROR_INVALID_LOGIN'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Display error if anything else goes wrong
|
|
||||||
else
|
|
||||||
$scope.loginError = error.translatableMessage;
|
|
||||||
|
|
||||||
// Reset all remaining fields to default values, but
|
|
||||||
// preserve any usernames
|
|
||||||
angular.forEach($scope.remainingFields, function clearEnteredValueIfPassword(field) {
|
|
||||||
if (field.type !== Field.Type.USERNAME && field.name in $scope.enteredValues)
|
|
||||||
$scope.enteredValues[field.name] = DEFAULT_FIELD_VALUE;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -244,6 +200,48 @@ angular.module('login').directive('guacLogin', [function guacLogin() {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update UI to reflect in-progress auth status (clear any previous
|
||||||
|
// errors, flag as pending)
|
||||||
|
$rootScope.$on('guacLoginPending', function loginSuccessful() {
|
||||||
|
$scope.submitted = true;
|
||||||
|
$scope.loginError = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retry route upon success (entered values will be cleared only
|
||||||
|
// after route change has succeeded as this can take time)
|
||||||
|
$rootScope.$on('guacLogin', function loginSuccessful() {
|
||||||
|
$route.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset upon failure
|
||||||
|
$rootScope.$on('guacLoginFailed', function loginFailed(event, parameters, error) {
|
||||||
|
|
||||||
|
// Initial submission is complete and has failed
|
||||||
|
$scope.submitted = false;
|
||||||
|
|
||||||
|
// Clear out passwords if the credentials were rejected for any reason
|
||||||
|
if (error.type !== Error.Type.INSUFFICIENT_CREDENTIALS) {
|
||||||
|
|
||||||
|
// Flag generic error for invalid login
|
||||||
|
if (error.type === Error.Type.INVALID_CREDENTIALS)
|
||||||
|
$scope.loginError = {
|
||||||
|
'key' : 'LOGIN.ERROR_INVALID_LOGIN'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Display error if anything else goes wrong
|
||||||
|
else
|
||||||
|
$scope.loginError = error.translatableMessage;
|
||||||
|
|
||||||
|
// Reset all remaining fields to default values, but
|
||||||
|
// preserve any usernames
|
||||||
|
angular.forEach($scope.remainingFields, function clearEnteredValueIfPassword(field) {
|
||||||
|
if (field.type !== Field.Type.USERNAME && field.name in $scope.enteredValues)
|
||||||
|
$scope.enteredValues[field.name] = DEFAULT_FIELD_VALUE;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
// Reset state after authentication and routing have succeeded
|
// Reset state after authentication and routing have succeeded
|
||||||
$rootScope.$on('$routeChangeSuccess', function routeChanged() {
|
$rootScope.$on('$routeChangeSuccess', function routeChanged() {
|
||||||
$scope.enteredValues = {};
|
$scope.enteredValues = {};
|
||||||
|
Reference in New Issue
Block a user