mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +00:00
GUACAMOLE-136: Implement basic support for verifying user identity using Duo.
This commit is contained in:
2
extensions/guacamole-auth-duo/.gitignore
vendored
Normal file
2
extensions/guacamole-auth-duo/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
target/
|
||||
*~
|
100
extensions/guacamole-auth-duo/pom.xml
Normal file
100
extensions/guacamole-auth-duo/pom.xml
Normal file
@@ -0,0 +1,100 @@
|
||||
<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-duo</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>0.9.10-incubating</version>
|
||||
<name>guacamole-auth-duo</name>
|
||||
<url>http://guacamole.incubator.apache.org/</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<!-- Written for 1.6 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.3</version>
|
||||
<configuration>
|
||||
<source>1.6</source>
|
||||
<target>1.6</target>
|
||||
<compilerArgs>
|
||||
<arg>-Xlint:all</arg>
|
||||
<arg>-Werror</arg>
|
||||
</compilerArgs>
|
||||
<fork>true</fork>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Copy dependencies prior to packaging -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>2.10</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>unpack-dependencies</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>unpack-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<includeScope>runtime</includeScope>
|
||||
<outputDirectory>${project.build.directory}/classes</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- Guacamole Java API -->
|
||||
<dependency>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-common</artifactId>
|
||||
<version>0.9.10-incubating</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Guacamole Extension API -->
|
||||
<dependency>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-ext</artifactId>
|
||||
<version>0.9.10-incubating</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Guice -->
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
<version>3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.inject.extensions</groupId>
|
||||
<artifactId>guice-multibindings</artifactId>
|
||||
<version>3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Java servlet API -->
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>servlet-api</artifactId>
|
||||
<version>2.5</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,138 @@
|
||||
package com.duosecurity.duoweb;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public final class DuoWeb {
|
||||
private static final String DUO_PREFIX = "TX";
|
||||
private static final String APP_PREFIX = "APP";
|
||||
private static final String AUTH_PREFIX = "AUTH";
|
||||
|
||||
private static final int DUO_EXPIRE = 300;
|
||||
private static final int APP_EXPIRE = 3600;
|
||||
|
||||
private static final int IKEY_LEN = 20;
|
||||
private static final int SKEY_LEN = 40;
|
||||
private static final int AKEY_LEN = 40;
|
||||
|
||||
public static final String ERR_USER = "ERR|The username passed to sign_request() is invalid.";
|
||||
public static final String ERR_IKEY = "ERR|The Duo integration key passed to sign_request() is invalid.";
|
||||
public static final String ERR_SKEY = "ERR|The Duo secret key passed to sign_request() is invalid.";
|
||||
public static final String ERR_AKEY = "ERR|The application secret key passed to sign_request() must be at least " + AKEY_LEN + " characters.";
|
||||
public static final String ERR_UNKNOWN = "ERR|An unknown error has occurred.";
|
||||
|
||||
public static String signRequest(final String ikey, final String skey, final String akey, final String username) {
|
||||
return signRequest(ikey, skey, akey, username, System.currentTimeMillis() / 1000);
|
||||
}
|
||||
|
||||
public static String signRequest(final String ikey, final String skey, final String akey, final String username, final long time) {
|
||||
final String duo_sig;
|
||||
final String app_sig;
|
||||
|
||||
if (username.equals("")) {
|
||||
return ERR_USER;
|
||||
}
|
||||
if (username.indexOf('|') != -1) {
|
||||
return ERR_USER;
|
||||
}
|
||||
if (ikey.equals("") || ikey.length() != IKEY_LEN) {
|
||||
return ERR_IKEY;
|
||||
}
|
||||
if (skey.equals("") || skey.length() != SKEY_LEN) {
|
||||
return ERR_SKEY;
|
||||
}
|
||||
if (akey.equals("") || akey.length() < AKEY_LEN) {
|
||||
return ERR_AKEY;
|
||||
}
|
||||
|
||||
try {
|
||||
duo_sig = signVals(skey, username, ikey, DUO_PREFIX, DUO_EXPIRE, time);
|
||||
app_sig = signVals(akey, username, ikey, APP_PREFIX, APP_EXPIRE, time);
|
||||
} catch (Exception e) {
|
||||
return ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
return duo_sig + ":" + app_sig;
|
||||
}
|
||||
|
||||
public static String verifyResponse(final String ikey, final String skey, final String akey, final String sig_response)
|
||||
throws DuoWebException, NoSuchAlgorithmException, InvalidKeyException, IOException {
|
||||
return verifyResponse(ikey, skey, akey, sig_response, System.currentTimeMillis() / 1000);
|
||||
}
|
||||
|
||||
public static String verifyResponse(final String ikey, final String skey, final String akey, final String sig_response, final long time)
|
||||
throws DuoWebException, NoSuchAlgorithmException, InvalidKeyException, IOException {
|
||||
String auth_user = null;
|
||||
String app_user = null;
|
||||
|
||||
final String[] sigs = sig_response.split(":");
|
||||
final String auth_sig = sigs[0];
|
||||
final String app_sig = sigs[1];
|
||||
|
||||
auth_user = parseVals(skey, auth_sig, AUTH_PREFIX, ikey, time);
|
||||
app_user = parseVals(akey, app_sig, APP_PREFIX, ikey, time);
|
||||
|
||||
if (!auth_user.equals(app_user)) {
|
||||
throw new DuoWebException("Authentication failed.");
|
||||
}
|
||||
|
||||
return auth_user;
|
||||
}
|
||||
|
||||
private static String signVals(final String key, final String username, final String ikey, final String prefix, final int expire, final long time)
|
||||
throws InvalidKeyException, NoSuchAlgorithmException {
|
||||
final long expire_ts = time + expire;
|
||||
final String exp = Long.toString(expire_ts);
|
||||
|
||||
final String val = username + "|" + ikey + "|" + exp;
|
||||
final String cookie = prefix + "|" + Base64.encodeBytes(val.getBytes());
|
||||
final String sig = Util.hmacSign(key, cookie);
|
||||
|
||||
return cookie + "|" + sig;
|
||||
}
|
||||
|
||||
private static String parseVals(final String key, final String val, final String prefix, final String ikey, final long time)
|
||||
throws InvalidKeyException, NoSuchAlgorithmException, IOException, DuoWebException {
|
||||
|
||||
final String[] parts = val.split("\\|");
|
||||
if (parts.length != 3) {
|
||||
throw new DuoWebException("Invalid response");
|
||||
}
|
||||
|
||||
final String u_prefix = parts[0];
|
||||
final String u_b64 = parts[1];
|
||||
final String u_sig = parts[2];
|
||||
|
||||
final String sig = Util.hmacSign(key, u_prefix + "|" + u_b64);
|
||||
if (!Util.hmacSign(key, sig).equals(Util.hmacSign(key, u_sig))) {
|
||||
throw new DuoWebException("Invalid response");
|
||||
}
|
||||
|
||||
if (!u_prefix.equals(prefix)) {
|
||||
throw new DuoWebException("Invalid response");
|
||||
}
|
||||
|
||||
final byte[] decoded = Base64.decode(u_b64);
|
||||
final String cookie = new String(decoded);
|
||||
|
||||
final String[] cookie_parts = cookie.split("\\|");
|
||||
if (cookie_parts.length != 3) {
|
||||
throw new DuoWebException("Invalid response");
|
||||
}
|
||||
final String username = cookie_parts[0];
|
||||
final String u_ikey = cookie_parts[1];
|
||||
final String expire = cookie_parts[2];
|
||||
|
||||
if (!u_ikey.equals(ikey)) {
|
||||
throw new DuoWebException("Invalid response");
|
||||
}
|
||||
|
||||
final long expire_ts = Long.parseLong(expire);
|
||||
if (time >= expire_ts) {
|
||||
throw new DuoWebException("Transaction has expired. Please check that the system time is correct.");
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.duosecurity.duoweb;
|
||||
|
||||
public class DuoWebException extends Exception {
|
||||
|
||||
public DuoWebException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package com.duosecurity.duoweb;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class Util {
|
||||
public static String hmacSign(String skey, String data)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
SecretKeySpec key = new SecretKeySpec(skey.getBytes(), "HmacSHA1");
|
||||
Mac mac = Mac.getInstance("HmacSHA1");
|
||||
mac.init(key);
|
||||
byte[] raw = mac.doFinal(data.getBytes());
|
||||
return bytesToHex(raw);
|
||||
}
|
||||
|
||||
public static String bytesToHex(byte[] b) {
|
||||
String result = "";
|
||||
for (int i = 0; i < b.length; i++) {
|
||||
result += Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.duo;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import java.util.Collections;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.apache.guacamole.GuacamoleClientException;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.auth.duo.conf.ConfigurationService;
|
||||
import org.apache.guacamole.auth.duo.form.DuoSignedResponseField;
|
||||
import org.apache.guacamole.form.Field;
|
||||
import org.apache.guacamole.net.auth.AuthenticatedUser;
|
||||
import org.apache.guacamole.net.auth.Credentials;
|
||||
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
|
||||
import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
|
||||
|
||||
/**
|
||||
* Service providing convenience functions for the Duo AuthenticationProvider
|
||||
* implementation.
|
||||
*/
|
||||
public class AuthenticationProviderService {
|
||||
|
||||
/**
|
||||
* Service for retrieving Duo configuration information.
|
||||
*/
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Service for verifying users with the DuoWeb API.
|
||||
*/
|
||||
@Inject
|
||||
private DuoWebService duoWebService;
|
||||
|
||||
/**
|
||||
* Verifies the identity of the given user via the Duo multi-factor
|
||||
* authentication service. If a signed response from Duo has not already
|
||||
* been provided, a signed response from Duo is requested in the
|
||||
* form of additional expected credentials. Any provided signed response
|
||||
* is cryptographically verified. If no signed response is present, or the
|
||||
* signed response is invalid, an exception is thrown.
|
||||
*
|
||||
* @param authenticatedUser
|
||||
* The user whose identity should be verified against Duo.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If required Duo-specific configuration options are missing or
|
||||
* malformed, or if the user's identity cannot be verified.
|
||||
*/
|
||||
public void verifyAuthenticatedUser(AuthenticatedUser authenticatedUser)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Pull the original HTTP request used to authenticate
|
||||
Credentials credentials = authenticatedUser.getCredentials();
|
||||
HttpServletRequest request = credentials.getRequest();
|
||||
|
||||
// Ignore anonymous users
|
||||
if (authenticatedUser.getIdentifier().equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
|
||||
return;
|
||||
|
||||
// Retrieve signed Duo response from request
|
||||
String signedResponse = request.getParameter(DuoSignedResponseField.PARAMETER_NAME);
|
||||
|
||||
// If no signed response, request one
|
||||
if (signedResponse == null) {
|
||||
|
||||
// Create field which requests a signed response from Duo that
|
||||
// verifies the identity of the given user via the configured
|
||||
// Duo API endpoint
|
||||
Field signedResponseField = new DuoSignedResponseField(
|
||||
confService.getAPIHostname(),
|
||||
duoWebService.createSignedRequest(authenticatedUser));
|
||||
|
||||
// Create an overall description of the additional credentials
|
||||
// required to verify identity
|
||||
CredentialsInfo expectedCredentials = new CredentialsInfo(
|
||||
Collections.singletonList(signedResponseField));
|
||||
|
||||
// Request additional credentials
|
||||
throw new GuacamoleInsufficientCredentialsException(
|
||||
"LOGIN.INFO_DUO_AUTH_REQUIRED", expectedCredentials);
|
||||
|
||||
}
|
||||
|
||||
// If signed response does not verify this user's identity, abort auth
|
||||
if (!duoWebService.isValidSignedResponse(authenticatedUser, signedResponse))
|
||||
throw new GuacamoleClientException("LOGIN.INFO_DUO_VALIDATION_CODE_INCORRECT");
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.duo;
|
||||
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.net.auth.AuthenticatedUser;
|
||||
import org.apache.guacamole.net.auth.AuthenticationProvider;
|
||||
import org.apache.guacamole.net.auth.Credentials;
|
||||
import org.apache.guacamole.net.auth.UserContext;
|
||||
|
||||
/**
|
||||
* AuthenticationProvider implementation which uses Duo as an additional
|
||||
* authentication factor for users which have already been authenticated by
|
||||
* some other AuthenticationProvider.
|
||||
*/
|
||||
public class DuoAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
/**
|
||||
* Injector which will manage the object graph of this authentication
|
||||
* provider.
|
||||
*/
|
||||
private final Injector injector;
|
||||
|
||||
/**
|
||||
* Creates a new DuoAuthenticationProvider that verifies users
|
||||
* using the Duo authentication service
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If a required property is missing, or an error occurs while parsing
|
||||
* a property.
|
||||
*/
|
||||
public DuoAuthenticationProvider() throws GuacamoleException {
|
||||
|
||||
// Set up Guice injector.
|
||||
injector = Guice.createInjector(
|
||||
new DuoAuthenticationProviderModule(this)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return "duo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticatedUser authenticateUser(Credentials credentials)
|
||||
throws GuacamoleException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser,
|
||||
Credentials credentials) throws GuacamoleException {
|
||||
return authenticatedUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserContext getUserContext(AuthenticatedUser authenticatedUser)
|
||||
throws GuacamoleException {
|
||||
|
||||
AuthenticationProviderService authProviderService =
|
||||
injector.getInstance(AuthenticationProviderService.class);
|
||||
|
||||
// Verify user against Duo service
|
||||
authProviderService.verifyAuthenticatedUser(authenticatedUser);
|
||||
|
||||
// User has been verified, and authentication should be allowed to
|
||||
// continue
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserContext updateUserContext(UserContext context,
|
||||
AuthenticatedUser authenticatedUser, Credentials credentials)
|
||||
throws GuacamoleException {
|
||||
return context;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.duo;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.auth.duo.conf.ConfigurationService;
|
||||
import org.apache.guacamole.environment.Environment;
|
||||
import org.apache.guacamole.environment.LocalEnvironment;
|
||||
import org.apache.guacamole.net.auth.AuthenticationProvider;
|
||||
|
||||
/**
|
||||
* Guice module which configures Duo-specific injections.
|
||||
*/
|
||||
public class DuoAuthenticationProviderModule extends AbstractModule {
|
||||
|
||||
/**
|
||||
* Guacamole server environment.
|
||||
*/
|
||||
private final Environment environment;
|
||||
|
||||
/**
|
||||
* A reference to the DuoAuthenticationProvider on behalf of which this
|
||||
* module has configured injection.
|
||||
*/
|
||||
private final AuthenticationProvider authProvider;
|
||||
|
||||
/**
|
||||
* Creates a new Duo authentication provider module which configures
|
||||
* injection for the DuoAuthenticationProvider.
|
||||
*
|
||||
* @param authProvider
|
||||
* The AuthenticationProvider for which injection is being configured.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If an error occurs while retrieving the Guacamole server
|
||||
* environment.
|
||||
*/
|
||||
public DuoAuthenticationProviderModule(AuthenticationProvider authProvider)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Get local environment
|
||||
this.environment = new LocalEnvironment();
|
||||
|
||||
// Store associated auth provider
|
||||
this.authProvider = authProvider;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
|
||||
// Bind core implementations of guacamole-ext classes
|
||||
bind(AuthenticationProvider.class).toInstance(authProvider);
|
||||
bind(Environment.class).toInstance(environment);
|
||||
|
||||
// Bind Duo-specific services
|
||||
bind(AuthenticationProviderService.class);
|
||||
bind(ConfigurationService.class);
|
||||
bind(DuoWebService.class);
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* 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.duo;
|
||||
|
||||
import com.duosecurity.duoweb.DuoWeb;
|
||||
import com.duosecurity.duoweb.DuoWebException;
|
||||
import com.google.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.apache.guacamole.GuacamoleClientException;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.GuacamoleServerException;
|
||||
import org.apache.guacamole.auth.duo.conf.ConfigurationService;
|
||||
import org.apache.guacamole.net.auth.AuthenticatedUser;
|
||||
|
||||
/**
|
||||
* Service which wraps the DuoWeb Java API, providing predictable behavior and
|
||||
* error handling.
|
||||
*/
|
||||
public class DuoWebService {
|
||||
|
||||
/**
|
||||
* A regular expression which matches a valid signature part of a Duo
|
||||
* signed response. A signature part may not contain pipe symbols (which
|
||||
* act as delimiters between parts) nor colons (which act as delimiters
|
||||
* between signatures).
|
||||
*/
|
||||
private final String SIGNATURE_PART = "[^:|]*";
|
||||
|
||||
/**
|
||||
* A regular expression which matches a valid signature within a Duo
|
||||
* signed response. Each signature is made up of three distinct parts,
|
||||
* separated by pipe symbols.
|
||||
*/
|
||||
private final String SIGNATURE = SIGNATURE_PART + "\\|" + SIGNATURE_PART + "\\|" + SIGNATURE_PART;
|
||||
|
||||
/**
|
||||
* A regular expression which matches a valid Duo signed response. Each
|
||||
* response is made up of two signatures, separated by a colon.
|
||||
*/
|
||||
private final String RESPONSE = SIGNATURE + ":" + SIGNATURE;
|
||||
|
||||
/**
|
||||
* A Pattern which matches valid Duo signed responses. Strings which will
|
||||
* be passed to DuoWeb.verifyResponse() MUST be matched against this
|
||||
* Pattern. Strings which do not match this Pattern may cause
|
||||
* DuoWeb.verifyResponse() to throw unchecked exceptions.
|
||||
*/
|
||||
private final Pattern RESPONSE_PATTERN = Pattern.compile(RESPONSE);
|
||||
|
||||
/**
|
||||
* Service for retrieving Duo configuration information.
|
||||
*/
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Creates and signs a new request to verify the identity of the given
|
||||
* user. This request may ultimately be sent to Duo, resulting in a signed
|
||||
* response from Duo if that verification succeeds.
|
||||
*
|
||||
* @param authenticatedUser
|
||||
* The user whose identity should be verified.
|
||||
*
|
||||
* @return
|
||||
* A signed user verification request which can be sent to Duo.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If required Duo-specific configuration options are missing or
|
||||
* invalid, or if an error occurs within the DuoWeb API which prevents
|
||||
* generation of the signed request.
|
||||
*/
|
||||
public String createSignedRequest(AuthenticatedUser authenticatedUser)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Retrieve username from externally-authenticated user
|
||||
String username = authenticatedUser.getIdentifier();
|
||||
|
||||
// Retrieve Duo-specific keys from configuration
|
||||
String ikey = confService.getIntegrationKey();
|
||||
String skey = confService.getSecretKey();
|
||||
String akey = confService.getApplicationKey();
|
||||
|
||||
// Create signed request for the provided user
|
||||
String signedRequest = DuoWeb.signRequest(ikey, skey, akey, username);
|
||||
|
||||
if (DuoWeb.ERR_AKEY.equals(signedRequest))
|
||||
throw new GuacamoleServerException("The Duo application key "
|
||||
+ "must is not valid. Duo application keys must be at "
|
||||
+ "least 40 characters long.");
|
||||
|
||||
if (DuoWeb.ERR_IKEY.equals(signedRequest))
|
||||
throw new GuacamoleServerException("The provided Duo integration "
|
||||
+ "key is not valid. Integration keys must be exactly 20 "
|
||||
+ "characters long.");
|
||||
|
||||
if (DuoWeb.ERR_SKEY.equals(signedRequest))
|
||||
throw new GuacamoleServerException("The provided Duo secret key "
|
||||
+ "is not valid. Secret keys must be exactly 40 "
|
||||
+ "characters long.");
|
||||
|
||||
if (DuoWeb.ERR_USER.equals(signedRequest))
|
||||
throw new GuacamoleServerException("The provided username is "
|
||||
+ "not valid. Duo usernames may not be blank, nor may "
|
||||
+ "they contain pipe symbols (\"|\").");
|
||||
|
||||
if (DuoWeb.ERR_UNKNOWN.equals(signedRequest))
|
||||
throw new GuacamoleServerException("An unknown error within the "
|
||||
+ "DuoWeb API prevented the signed request from being "
|
||||
+ "generated.");
|
||||
|
||||
// Return signed request if no error is indicated
|
||||
return signedRequest;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given signed response is a valid response from Duo
|
||||
* which verifies the identity of the given user. If the given response is
|
||||
* invalid or does not verify the identity of the given user (including if
|
||||
* it is a valid response which verifies the identity of a DIFFERENT user),
|
||||
* false is returned.
|
||||
*
|
||||
* @param authenticatedUser
|
||||
* The user that the given signed response should verify.
|
||||
*
|
||||
* @param signedResponse
|
||||
* The signed response received from Duo in response to a signed
|
||||
* request.
|
||||
*
|
||||
* @return
|
||||
* true if the signed response is a valid response from Duo AND verifies
|
||||
* the identity of the given user, false otherwise.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If required Duo-specific configuration options are missing or
|
||||
* invalid, or if an error occurs within the DuoWeb API which prevents
|
||||
* validation of the signed response.
|
||||
*/
|
||||
public boolean isValidSignedResponse(AuthenticatedUser authenticatedUser,
|
||||
String signedResponse) throws GuacamoleException {
|
||||
|
||||
// Verify signature response format will not cause
|
||||
// DuoWeb.verifyResponse() to fail with unchecked exceptions
|
||||
Matcher responseMatcher = RESPONSE_PATTERN.matcher(signedResponse);
|
||||
if (!responseMatcher.matches())
|
||||
throw new GuacamoleClientException("Invalid Duo response format.");
|
||||
|
||||
// Retrieve username from externally-authenticated user
|
||||
String username = authenticatedUser.getIdentifier();
|
||||
|
||||
// Retrieve Duo-specific keys from configuration
|
||||
String ikey = confService.getIntegrationKey();
|
||||
String skey = confService.getSecretKey();
|
||||
String akey = confService.getApplicationKey();
|
||||
|
||||
// Verify validity of signed response
|
||||
String verifiedUsername;
|
||||
try {
|
||||
verifiedUsername = DuoWeb.verifyResponse(ikey, skey, akey,
|
||||
signedResponse);
|
||||
}
|
||||
|
||||
// Rethrow any errors as appropriate GuacamoleExceptions
|
||||
catch (IOException e) {
|
||||
throw new GuacamoleClientException("Decoding of Duo response "
|
||||
+ "failed: Invalid base64 content.", e);
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
throw new GuacamoleClientException("Decoding of Duo response "
|
||||
+ "failed: Invalid expiry timestamp.", e);
|
||||
}
|
||||
catch (InvalidKeyException e) {
|
||||
throw new GuacamoleServerException("Unable to produce HMAC "
|
||||
+ "signature: " + e.getMessage(), e);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
throw new GuacamoleServerException("Environment is missing "
|
||||
+ "support for producing HMAC-SHA1 signatures.", e);
|
||||
}
|
||||
catch (DuoWebException e) {
|
||||
throw new GuacamoleClientException("Duo response verification "
|
||||
+ "failed: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Signed response is valid iff the associated username matches the
|
||||
// user's username
|
||||
return username.equals(verifiedUsername);
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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.duo.conf;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.environment.Environment;
|
||||
import org.apache.guacamole.properties.StringGuacamoleProperty;
|
||||
|
||||
/**
|
||||
* Service for retrieving configuration information regarding the Duo
|
||||
* authentication extension.
|
||||
*/
|
||||
public class ConfigurationService {
|
||||
|
||||
/**
|
||||
* The Guacamole server environment.
|
||||
*/
|
||||
@Inject
|
||||
private Environment environment;
|
||||
|
||||
/**
|
||||
* The property within guacamole.properties which defines the hostname
|
||||
* of the Duo API endpoint to be used to verify user identities. This will
|
||||
* usually be in the form "api-XXXXXXXX.duosecurity.com", where "XXXXXXXX"
|
||||
* is some arbitrary alphanumeric value assigned by Duo and specific to
|
||||
* your organization.
|
||||
*/
|
||||
private static final StringGuacamoleProperty DUO_API_HOSTNAME =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "duo-api-hostname"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The property within guacamole.properties which defines the integration
|
||||
* key received from Duo for verifying Guacamole users. This value MUST be
|
||||
* exactly 20 characters.
|
||||
*/
|
||||
private static final StringGuacamoleProperty DUO_INTEGRATION_KEY =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "duo-integration-key"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The property within guacamole.properties which defines the secret key
|
||||
* received from Duo for verifying Guacamole users. This value MUST be
|
||||
* exactly 40 characters.
|
||||
*/
|
||||
private static final StringGuacamoleProperty DUO_SECRET_KEY =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "duo-secret-key"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The property within guacamole.properties which defines the arbitrary
|
||||
* random key which was generated for Guacamole. Note that this value is not
|
||||
* provided by Duo, but is expected to be generated by the administrator of
|
||||
* the system hosting Guacamole. This value MUST be at least 40 characters.
|
||||
*/
|
||||
private static final StringGuacamoleProperty DUO_APPLICATION_KEY =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "duo-application-key"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the hostname of the Duo API endpoint to be used to verify user
|
||||
* identities, as defined in guacamole.properties by the "duo-api-hostname"
|
||||
* property. This will usually be in the form
|
||||
* "api-XXXXXXXX.duosecurity.com", where "XXXXXXXX" is some arbitrary
|
||||
* alphanumeric value assigned by Duo and specific to your organization.
|
||||
*
|
||||
* @return
|
||||
* The hostname of the Duo API endpoint to be used to verify user
|
||||
* identities.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the associated property within guacamole.properties is missing.
|
||||
*/
|
||||
public String getAPIHostname() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(DUO_API_HOSTNAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integration key received from Duo for verifying Guacamole
|
||||
* users, as defined in guacamole.properties by the "duo-integration-key"
|
||||
* property. This value MUST be exactly 20 characters.
|
||||
*
|
||||
* @return
|
||||
* The integration key received from Duo for verifying Guacamole
|
||||
* users.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the associated property within guacamole.properties is missing.
|
||||
*/
|
||||
public String getIntegrationKey() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(DUO_INTEGRATION_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the secret key received from Duo for verifying Guacamole users,
|
||||
* as defined in guacamole.properties by the "duo-secret-key" property. This
|
||||
* value MUST be exactly 20 characters.
|
||||
*
|
||||
* @return
|
||||
* The secret key received from Duo for verifying Guacamole users.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the associated property within guacamole.properties is missing.
|
||||
*/
|
||||
public String getSecretKey() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(DUO_SECRET_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the arbitrary random key which was generated for Guacamole, as
|
||||
* defined in guacamole.properties by the "duo-application-key" property.
|
||||
* Note that this value is not provided by Duo, but is expected to be
|
||||
* generated by the administrator of the system hosting Guacamole. This
|
||||
* value MUST be at least 40 characters.
|
||||
*
|
||||
* @return
|
||||
* The arbitrary random key which was generated for Guacamole.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the associated property within guacamole.properties is missing.
|
||||
*/
|
||||
public String getApplicationKey() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(DUO_APPLICATION_KEY);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.duo.form;
|
||||
|
||||
import org.apache.guacamole.form.Field;
|
||||
import org.codehaus.jackson.annotate.JsonProperty;
|
||||
|
||||
/**
|
||||
* A custom field type which uses the DuoWeb API to produce a signed response
|
||||
* for a particular user. The signed response serves as an additional
|
||||
* authentication factor, as it cryptographically verifies possession of the
|
||||
* physical device associated with that user's Duo account.
|
||||
*/
|
||||
public class DuoSignedResponseField extends Field {
|
||||
|
||||
/**
|
||||
* The name of the HTTP parameter which an instance of this field will
|
||||
* populate within a user's credentials.
|
||||
*/
|
||||
public static final String PARAMETER_NAME = "guac-duo-signed-response";
|
||||
|
||||
/**
|
||||
* The unique name associated with this field type.
|
||||
*/
|
||||
private static final String FIELD_TYPE_NAME = "GUAC_DUO_SIGNED_RESPONSE";
|
||||
|
||||
/**
|
||||
* The hostname of the DuoWeb API endpoint.
|
||||
*/
|
||||
private final String apiHost;
|
||||
|
||||
/**
|
||||
* The signed request generated by a call to DuoWeb.signRequest().
|
||||
*/
|
||||
private final String signedRequest;
|
||||
|
||||
/**
|
||||
* Creates a new field which uses the DuoWeb API to prompt the user for
|
||||
* additional credentials. The provided credentials, if valid, will
|
||||
* ultimately be verified by Duo's service, resulting in a signed response
|
||||
* which can be cryptographically verified.
|
||||
*
|
||||
* @param apiHost
|
||||
* The hostname of the DuoWeb API endpoint.
|
||||
*
|
||||
* @param signedRequest
|
||||
* A signed request generated for the user in question by a call to
|
||||
* DuoWeb.signRequest().
|
||||
*/
|
||||
public DuoSignedResponseField(String apiHost, String signedRequest) {
|
||||
|
||||
// Init base field type properties
|
||||
super(PARAMETER_NAME, FIELD_TYPE_NAME);
|
||||
|
||||
// Init Duo-specific properties
|
||||
this.apiHost = apiHost;
|
||||
this.signedRequest = signedRequest;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hostname of the DuoWeb API endpoint.
|
||||
*
|
||||
* @return
|
||||
* The hostname of the DuoWeb API endpoint.
|
||||
*/
|
||||
@JsonProperty("apiHost")
|
||||
public String getAPIHost() {
|
||||
return apiHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the signed request string, which must have been generated by a
|
||||
* call to DuoWeb.signRequest().
|
||||
*
|
||||
* @return
|
||||
* The signed request generated by a call to DuoWeb.signRequest().
|
||||
*/
|
||||
public String getSignedRequest() {
|
||||
return signedRequest;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Config block which registers Duo-specific field types.
|
||||
*/
|
||||
angular.module('guacDuo').config(['formServiceProvider',
|
||||
function guacDuoConfig(formServiceProvider) {
|
||||
|
||||
// Define field for the signed response from the Duo service
|
||||
formServiceProvider.registerFieldType('GUAC_DUO_SIGNED_RESPONSE', {
|
||||
module : 'guacDuo',
|
||||
controller : 'duoSignedResponseController',
|
||||
templateUrl : 'app/ext/duo/templates/duoSignedResponseField.html'
|
||||
});
|
||||
|
||||
}]);
|
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Controller for the "GUAC_DUO_SIGNED_RESPONSE" field which uses the DuoWeb
|
||||
* API to prompt the user for additional credentials, ultimately receiving a
|
||||
* signed response from the Duo service.
|
||||
*/
|
||||
angular.module('guacDuo').controller('duoSignedResponseController', ['$scope',
|
||||
function duoSignedResponseController($scope) {
|
||||
|
||||
/**
|
||||
* The iframe which contains the Duo authentication interface.
|
||||
*
|
||||
* @type HTMLIFrameElement
|
||||
*/
|
||||
var iframe = $('.duo-signature-response-field iframe')[0];
|
||||
|
||||
/**
|
||||
* Whether the Duo interface has finished loading within the iframe.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
$scope.duoInterfaceLoaded = false;
|
||||
|
||||
/**
|
||||
* Submits the signed response from Duo once the user has authenticated.
|
||||
* This is a callback invoked by the DuoWeb API after the user has been
|
||||
* verified and the signed response has been received.
|
||||
*
|
||||
* @param {HTMLFormElement} form
|
||||
* The form element provided by the DuoWeb API containing the signed
|
||||
* response as the value of an input field named "sig_response".
|
||||
*/
|
||||
var submitSignedResponse = function submitSignedResponse(form) {
|
||||
|
||||
// Update model to match received response
|
||||
$scope.$apply(function updateModel() {
|
||||
$scope.model = form.elements['sig_response'].value;
|
||||
});
|
||||
|
||||
// Submit updated credentials
|
||||
$(iframe).parents('form').submit();
|
||||
|
||||
};
|
||||
|
||||
// Update Duo loaded state when iframe finishes loading
|
||||
iframe.onload = function duoLoaded() {
|
||||
$scope.$apply(function updateLoadedState() {
|
||||
$scope.duoInterfaceLoaded = true;
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize Duo interface within iframe
|
||||
Duo.init({
|
||||
iframe : iframe,
|
||||
host : $scope.field.apiHost,
|
||||
sig_request : $scope.field.signedRequest,
|
||||
submit_callback : submitSignedResponse
|
||||
});
|
||||
|
||||
}]);
|
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module which provides handling for Duo multi-factor authentication.
|
||||
*/
|
||||
angular.module('guacDuo', [
|
||||
'form'
|
||||
]);
|
||||
|
||||
// Ensure the guacDuo module is loaded along with the rest of the app
|
||||
angular.module('index').requires.push('guacDuo');
|
@@ -0,0 +1,35 @@
|
||||
{
|
||||
|
||||
"guacamoleVersion" : "0.9.10-incubating",
|
||||
|
||||
"name" : "Duo TFA Authentication Backend",
|
||||
"namespace" : "duo",
|
||||
|
||||
"authProviders" : [
|
||||
"org.apache.guacamole.auth.duo.DuoAuthenticationProvider"
|
||||
],
|
||||
|
||||
"translations" : [
|
||||
"translations/en.json"
|
||||
],
|
||||
|
||||
"js" : [
|
||||
|
||||
"duoModule.js",
|
||||
"controllers/duoSignedResponseController.js",
|
||||
"config/duoConfig.js",
|
||||
|
||||
"lib/DuoWeb/LICENSE.js",
|
||||
"lib/DuoWeb/Duo-Web-v2.js"
|
||||
|
||||
],
|
||||
|
||||
"css" : [
|
||||
"styles/duo.css"
|
||||
],
|
||||
|
||||
"resources" : {
|
||||
"templates/duoSignedResponseField.html" : "text/html"
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* Duo Web SDK v2
|
||||
* Copyright 2015, Duo Security
|
||||
*/
|
||||
window.Duo = (function(document, window) {
|
||||
var DUO_MESSAGE_FORMAT = /^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/;
|
||||
var DUO_ERROR_FORMAT = /^ERR\|[\w\s\.\(\)]+$/;
|
||||
|
||||
var iframeId = 'duo_iframe',
|
||||
postAction = '',
|
||||
postArgument = 'sig_response',
|
||||
host,
|
||||
sigRequest,
|
||||
duoSig,
|
||||
appSig,
|
||||
iframe,
|
||||
submitCallback;
|
||||
|
||||
function throwError(message, url) {
|
||||
throw new Error(
|
||||
'Duo Web SDK error: ' + message +
|
||||
(url ? ('\n' + 'See ' + url + ' for more information') : '')
|
||||
);
|
||||
}
|
||||
|
||||
function hyphenize(str) {
|
||||
return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase();
|
||||
}
|
||||
|
||||
// cross-browser data attributes
|
||||
function getDataAttribute(element, name) {
|
||||
if ('dataset' in element) {
|
||||
return element.dataset[name];
|
||||
} else {
|
||||
return element.getAttribute('data-' + hyphenize(name));
|
||||
}
|
||||
}
|
||||
|
||||
// cross-browser event binding/unbinding
|
||||
function on(context, event, fallbackEvent, callback) {
|
||||
if ('addEventListener' in window) {
|
||||
context.addEventListener(event, callback, false);
|
||||
} else {
|
||||
context.attachEvent(fallbackEvent, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function off(context, event, fallbackEvent, callback) {
|
||||
if ('removeEventListener' in window) {
|
||||
context.removeEventListener(event, callback, false);
|
||||
} else {
|
||||
context.detachEvent(fallbackEvent, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function onReady(callback) {
|
||||
on(document, 'DOMContentLoaded', 'onreadystatechange', callback);
|
||||
}
|
||||
|
||||
function offReady(callback) {
|
||||
off(document, 'DOMContentLoaded', 'onreadystatechange', callback);
|
||||
}
|
||||
|
||||
function onMessage(callback) {
|
||||
on(window, 'message', 'onmessage', callback);
|
||||
}
|
||||
|
||||
function offMessage(callback) {
|
||||
off(window, 'message', 'onmessage', callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the sig_request parameter, throwing errors if the token contains
|
||||
* a server error or if the token is invalid.
|
||||
*
|
||||
* @param {String} sig Request token
|
||||
*/
|
||||
function parseSigRequest(sig) {
|
||||
if (!sig) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
// see if the token contains an error, throwing it if it does
|
||||
if (sig.indexOf('ERR|') === 0) {
|
||||
throwError(sig.split('|')[1]);
|
||||
}
|
||||
|
||||
// validate the token
|
||||
if (sig.indexOf(':') === -1 || sig.split(':').length !== 2) {
|
||||
throwError(
|
||||
'Duo was given a bad token. This might indicate a configuration ' +
|
||||
'problem with one of Duo\'s client libraries.',
|
||||
'https://www.duosecurity.com/docs/duoweb#first-steps'
|
||||
);
|
||||
}
|
||||
|
||||
var sigParts = sig.split(':');
|
||||
|
||||
// hang on to the token, and the parsed duo and app sigs
|
||||
sigRequest = sig;
|
||||
duoSig = sigParts[0];
|
||||
appSig = sigParts[1];
|
||||
|
||||
return {
|
||||
sigRequest: sig,
|
||||
duoSig: sigParts[0],
|
||||
appSig: sigParts[1]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is set up to run when the DOM is ready, if the iframe was
|
||||
* not available during `init`.
|
||||
*/
|
||||
function onDOMReady() {
|
||||
iframe = document.getElementById(iframeId);
|
||||
|
||||
if (!iframe) {
|
||||
throw new Error(
|
||||
'This page does not contain an iframe for Duo to use.' +
|
||||
'Add an element like <iframe id="duo_iframe"></iframe> ' +
|
||||
'to this page. ' +
|
||||
'See https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe ' +
|
||||
'for more information.'
|
||||
);
|
||||
}
|
||||
|
||||
// we've got an iframe, away we go!
|
||||
ready();
|
||||
|
||||
// always clean up after yourself
|
||||
offReady(onDOMReady);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a MessageEvent came from the Duo service, and that it
|
||||
* is a properly formatted payload.
|
||||
*
|
||||
* The Google Chrome sign-in page injects some JS into pages that also
|
||||
* make use of postMessage, so we need to do additional validation above
|
||||
* and beyond the origin.
|
||||
*
|
||||
* @param {MessageEvent} event Message received via postMessage
|
||||
*/
|
||||
function isDuoMessage(event) {
|
||||
return Boolean(
|
||||
event.origin === ('https://' + host) &&
|
||||
typeof event.data === 'string' &&
|
||||
(
|
||||
event.data.match(DUO_MESSAGE_FORMAT) ||
|
||||
event.data.match(DUO_ERROR_FORMAT)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the request token and prepare for the iframe to become ready.
|
||||
*
|
||||
* All options below can be passed into an options hash to `Duo.init`, or
|
||||
* specified on the iframe using `data-` attributes.
|
||||
*
|
||||
* Options specified using the options hash will take precedence over
|
||||
* `data-` attributes.
|
||||
*
|
||||
* Example using options hash:
|
||||
* ```javascript
|
||||
* Duo.init({
|
||||
* iframe: "some_other_id",
|
||||
* host: "api-main.duo.test",
|
||||
* sig_request: "...",
|
||||
* post_action: "/auth",
|
||||
* post_argument: "resp"
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Example using `data-` attributes:
|
||||
* ```
|
||||
* <iframe id="duo_iframe"
|
||||
* data-host="api-main.duo.test"
|
||||
* data-sig-request="..."
|
||||
* data-post-action="/auth"
|
||||
* data-post-argument="resp"
|
||||
* >
|
||||
* </iframe>
|
||||
* ```
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {String} options.iframe The iframe, or id of an iframe to set up
|
||||
* @param {String} options.host Hostname
|
||||
* @param {String} options.sig_request Request token
|
||||
* @param {String} [options.post_action=''] URL to POST back to after successful auth
|
||||
* @param {String} [options.post_argument='sig_response'] Parameter name to use for response token
|
||||
* @param {Function} [options.submit_callback] If provided, duo will not submit the form instead execute
|
||||
* the callback function with reference to the "duo_form" form object
|
||||
* submit_callback can be used to prevent the webpage from reloading.
|
||||
*/
|
||||
function init(options) {
|
||||
if (options) {
|
||||
if (options.host) {
|
||||
host = options.host;
|
||||
}
|
||||
|
||||
if (options.sig_request) {
|
||||
parseSigRequest(options.sig_request);
|
||||
}
|
||||
|
||||
if (options.post_action) {
|
||||
postAction = options.post_action;
|
||||
}
|
||||
|
||||
if (options.post_argument) {
|
||||
postArgument = options.post_argument;
|
||||
}
|
||||
|
||||
if (options.iframe) {
|
||||
if ('tagName' in options.iframe) {
|
||||
iframe = options.iframe;
|
||||
} else if (typeof options.iframe === 'string') {
|
||||
iframeId = options.iframe;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.submit_callback === 'function') {
|
||||
submitCallback = options.submit_callback;
|
||||
}
|
||||
}
|
||||
|
||||
// if we were given an iframe, no need to wait for the rest of the DOM
|
||||
if (iframe) {
|
||||
ready();
|
||||
} else {
|
||||
// try to find the iframe in the DOM
|
||||
iframe = document.getElementById(iframeId);
|
||||
|
||||
// iframe is in the DOM, away we go!
|
||||
if (iframe) {
|
||||
ready();
|
||||
} else {
|
||||
// wait until the DOM is ready, then try again
|
||||
onReady(onDOMReady);
|
||||
}
|
||||
}
|
||||
|
||||
// always clean up after yourself!
|
||||
offReady(init);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when a message was received from another domain
|
||||
* using the `postMessage` API. Check that the event came from the Duo
|
||||
* service domain, and that the message is a properly formatted payload,
|
||||
* then perform the post back to the primary service.
|
||||
*
|
||||
* @param event Event object (contains origin and data)
|
||||
*/
|
||||
function onReceivedMessage(event) {
|
||||
if (isDuoMessage(event)) {
|
||||
// the event came from duo, do the post back
|
||||
doPostBack(event.data);
|
||||
|
||||
// always clean up after yourself!
|
||||
offMessage(onReceivedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Point the iframe at Duo, then wait for it to postMessage back to us.
|
||||
*/
|
||||
function ready() {
|
||||
if (!host) {
|
||||
host = getDataAttribute(iframe, 'host');
|
||||
|
||||
if (!host) {
|
||||
throwError(
|
||||
'No API hostname is given for Duo to use. Be sure to pass ' +
|
||||
'a `host` parameter to Duo.init, or through the `data-host` ' +
|
||||
'attribute on the iframe element.',
|
||||
'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!duoSig || !appSig) {
|
||||
parseSigRequest(getDataAttribute(iframe, 'sigRequest'));
|
||||
|
||||
if (!duoSig || !appSig) {
|
||||
throwError(
|
||||
'No valid signed request is given. Be sure to give the ' +
|
||||
'`sig_request` parameter to Duo.init, or use the ' +
|
||||
'`data-sig-request` attribute on the iframe element.',
|
||||
'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// if postAction/Argument are defaults, see if they are specified
|
||||
// as data attributes on the iframe
|
||||
if (postAction === '') {
|
||||
postAction = getDataAttribute(iframe, 'postAction') || postAction;
|
||||
}
|
||||
|
||||
if (postArgument === 'sig_response') {
|
||||
postArgument = getDataAttribute(iframe, 'postArgument') || postArgument;
|
||||
}
|
||||
|
||||
// point the iframe at Duo
|
||||
iframe.src = [
|
||||
'https://', host, '/frame/web/v1/auth?tx=', duoSig,
|
||||
'&parent=', encodeURIComponent(document.location.href),
|
||||
'&v=2.3'
|
||||
].join('');
|
||||
|
||||
// listen for the 'message' event
|
||||
onMessage(onReceivedMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* We received a postMessage from Duo. POST back to the primary service
|
||||
* with the response token, and any additional user-supplied parameters
|
||||
* given in form#duo_form.
|
||||
*/
|
||||
function doPostBack(response) {
|
||||
// create a hidden input to contain the response token
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = postArgument;
|
||||
input.value = response + ':' + appSig;
|
||||
|
||||
// user may supply their own form with additional inputs
|
||||
var form = document.getElementById('duo_form');
|
||||
|
||||
// if the form doesn't exist, create one
|
||||
if (!form) {
|
||||
form = document.createElement('form');
|
||||
|
||||
// insert the new form after the iframe
|
||||
iframe.parentElement.insertBefore(form, iframe.nextSibling);
|
||||
}
|
||||
|
||||
// make sure we are actually posting to the right place
|
||||
form.method = 'POST';
|
||||
form.action = postAction;
|
||||
|
||||
// add the response token input to the form
|
||||
form.appendChild(input);
|
||||
|
||||
// away we go!
|
||||
if (typeof submitCallback === "function") {
|
||||
submitCallback.call(null, form);
|
||||
} else {
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
// when the DOM is ready, initialize
|
||||
// note that this will get cleaned up if the user calls init directly!
|
||||
onReady(init);
|
||||
|
||||
return {
|
||||
init: init,
|
||||
_parseSigRequest: parseSigRequest,
|
||||
_isDuoMessage: isDuoMessage,
|
||||
_doPostBack: doPostBack
|
||||
};
|
||||
}(document, window));
|
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2011, Duo Security, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. The name of the author may not be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.duo-signature-response-field iframe {
|
||||
width: 100%;
|
||||
max-width: 620px;
|
||||
height: 330px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.duo-signature-response-field iframe {
|
||||
opacity: 1;
|
||||
-webkit-transition: opacity 0.125s;
|
||||
-moz-transition: opacity 0.125s;
|
||||
-ms-transition: opacity 0.125s;
|
||||
-o-transition: opacity 0.125s;
|
||||
transition: opacity 0.125s;
|
||||
}
|
||||
|
||||
.duo-signature-response-field.loading iframe {
|
||||
opacity: 0;
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
<div class="duo-signature-response-field" ng-class="{ loading : !duoInterfaceLoaded }">
|
||||
<iframe></iframe>
|
||||
</div>
|
@@ -0,0 +1,13 @@
|
||||
{
|
||||
|
||||
"DATA_SOURCE_DUO" : {
|
||||
"NAME" : "Duo TFA Backend"
|
||||
},
|
||||
|
||||
"LOGIN" : {
|
||||
"FIELD_HEADER_GUAC_DUO_SIGNED_RESPONSE" : "",
|
||||
"INFO_DUO_VALIDATION_CODE_INCORRECT" : "Duo validation code incorrect.",
|
||||
"INFO_DUO_AUTH_REQUIRED" : "Please authenticate with Duo to continue."
|
||||
}
|
||||
|
||||
}
|
1
pom.xml
1
pom.xml
@@ -49,6 +49,7 @@
|
||||
<module>guacamole-common-js</module>
|
||||
|
||||
<!-- Authentication extensions -->
|
||||
<module>extensions/guacamole-auth-duo</module>
|
||||
<module>extensions/guacamole-auth-jdbc</module>
|
||||
<module>extensions/guacamole-auth-ldap</module>
|
||||
<module>extensions/guacamole-auth-noauth</module>
|
||||
|
Reference in New Issue
Block a user