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>
|
<module>guacamole-common-js</module>
|
||||||
|
|
||||||
<!-- Authentication extensions -->
|
<!-- Authentication extensions -->
|
||||||
|
<module>extensions/guacamole-auth-duo</module>
|
||||||
<module>extensions/guacamole-auth-jdbc</module>
|
<module>extensions/guacamole-auth-jdbc</module>
|
||||||
<module>extensions/guacamole-auth-ldap</module>
|
<module>extensions/guacamole-auth-ldap</module>
|
||||||
<module>extensions/guacamole-auth-noauth</module>
|
<module>extensions/guacamole-auth-noauth</module>
|
||||||
|
Reference in New Issue
Block a user