GUACAMOLE-1289: Update the Duo extension to the v4 API

This commit is contained in:
Virtually Nick
2023-10-05 17:09:32 -04:00
committed by Alex Leitner
parent 13494baa4a
commit e8860e4dd8
17 changed files with 251 additions and 1631 deletions

View File

@@ -39,93 +39,9 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<enforcer.skip>true</enforcer.skip>
</properties> </properties>
<build>
<plugins>
<!-- Pre-cache Angular templates with maven-angular-plugin -->
<plugin>
<groupId>com.keithbranton.mojo</groupId>
<artifactId>angular-maven-plugin</artifactId>
<version>0.3.4</version>
<executions>
<execution>
<phase>generate-resources</phase>
<goals>
<goal>html2js</goal>
</goals>
</execution>
</executions>
<configuration>
<sourceDir>${basedir}/src/main/resources</sourceDir>
<include>**/*.html</include>
<target>${basedir}/src/main/resources/generated/templates-main/templates.js</target>
<prefix>app/ext/duo</prefix>
</configuration>
</plugin>
<!-- JS/CSS Minification Plugin -->
<plugin>
<groupId>com.github.buckelieg</groupId>
<artifactId>minify-maven-plugin</artifactId>
<executions>
<execution>
<id>default-cli</id>
<configuration>
<charset>UTF-8</charset>
<webappSourceDir>${basedir}/src/main/resources</webappSourceDir>
<webappTargetDir>${project.build.directory}/classes</webappTargetDir>
<cssSourceDir>/</cssSourceDir>
<cssTargetDir>/</cssTargetDir>
<cssFinalFile>duo.css</cssFinalFile>
<cssSourceFiles>
<cssSourceFile>license.txt</cssSourceFile>
</cssSourceFiles>
<cssSourceIncludes>
<cssSourceInclude>**/*.css</cssSourceInclude>
</cssSourceIncludes>
<jsSourceDir>/</jsSourceDir>
<jsTargetDir>/</jsTargetDir>
<jsFinalFile>duo.js</jsFinalFile>
<jsSourceFiles>
<jsSourceFile>license.txt</jsSourceFile>
<jsSourceFile>lib/DuoWeb/LICENSE.js</jsSourceFile>
</jsSourceFiles>
<jsSourceIncludes>
<jsSourceInclude>**/*.js</jsSourceInclude>
</jsSourceIncludes>
<!-- Do not minify and include tests -->
<jsSourceExcludes>
<jsSourceExclude>**/*.test.js</jsSourceExclude>
</jsSourceExcludes>
<jsEngine>CLOSURE</jsEngine>
<!-- Disable warnings for JSDoc annotations -->
<closureWarningLevels>
<misplacedTypeAnnotation>OFF</misplacedTypeAnnotation>
<nonStandardJsDocs>OFF</nonStandardJsDocs>
</closureWarningLevels>
</configuration>
<goals>
<goal>minify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies> <dependencies>
<!-- Guacamole Extension API --> <!-- Guacamole Extension API -->
@@ -155,6 +71,20 @@
<version>2.5</version> <version>2.5</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- Duo SDK -->
<dependency>
<groupId>com.duosecurity</groupId>
<artifactId>duo-universal-sdk</artifactId>
<version>1.1.3</version>
</dependency>
<!-- kotlin-stdlib-common -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-common</artifactId>
<version>1.4.10</version>
</dependency>
</dependencies> </dependencies>

View File

@@ -21,7 +21,6 @@ package org.apache.guacamole.auth.duo;
import com.google.inject.AbstractModule; import com.google.inject.AbstractModule;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.duo.api.DuoService;
import org.apache.guacamole.auth.duo.conf.ConfigurationService; import org.apache.guacamole.auth.duo.conf.ConfigurationService;
import org.apache.guacamole.environment.Environment; import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment; import org.apache.guacamole.environment.LocalEnvironment;
@@ -74,8 +73,8 @@ public class DuoAuthenticationProviderModule extends AbstractModule {
// Bind Duo-specific services // Bind Duo-specific services
bind(ConfigurationService.class); bind(ConfigurationService.class);
bind(DuoService.class);
bind(UserVerificationService.class); bind(UserVerificationService.class);
bind(DuoAuthenticationSessionManager.class);
} }

View File

@@ -0,0 +1,74 @@
/*
* 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 org.apache.guacamole.net.auth.AuthenticationSession;
/**
* An AuthenticationSession that stores the information required for an
* in-progress Duo authentication attempt.
*/
public class DuoAuthenticationSession extends AuthenticationSession {
/**
* The session state generated by the Duo client, which is used to track
* the session through the redirect and return process.
*/
private final String state;
/**
* The username of the user who is authenticating with this session.
*/
private final String username;
/**
* Create a new instance of this authenticaiton session, having the given length of time
* for expriation and the state generated by the Duo Client.
*
* @param expires
* The number of milliseconds before this session is invalid.
*
* @param state
* The session state, as generated by the Duo Client.
*
* @param username
* The username of the user who is attempting authentication with Duo.
*/
public DuoAuthenticationSession(long expires, String state, String username) {
super(expires);
this.state = state;
this.username = username;
}
/**
* Return the stored session state.
*
* @return
* The stored session state.
*/
public String getState() {
return state;
}
public String getUsername() {
return username;
}
}

View File

@@ -17,12 +17,18 @@
* under the License. * under the License.
*/ */
/** package org.apache.guacamole.auth.duo;
* 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 import com.google.inject.Singleton;
angular.module('index').requires.push('guacDuo'); import org.apache.guacamole.net.auth.AuthenticationSessionManager;
/**
* An AuthenticationSessionManager implementation that temporarily stores
* authentication attempts for Duo MFA while they are underway.
*/
@Singleton
public class DuoAuthenticationSessionManager extends AuthenticationSessionManager<DuoAuthenticationSession> {
// Nothing to see here.
}

View File

@@ -19,16 +19,21 @@
package org.apache.guacamole.auth.duo; package org.apache.guacamole.auth.duo;
import com.duosecurity.Client;
import com.duosecurity.exception.DuoException;
import com.duosecurity.model.Token;
import com.google.inject.Inject; import com.google.inject.Inject;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections; import java.util.Collections;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.duo.api.DuoService; import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.duo.conf.ConfigurationService; import org.apache.guacamole.auth.duo.conf.ConfigurationService;
import org.apache.guacamole.auth.duo.form.DuoSignedResponseField; import org.apache.guacamole.form.RedirectField;
import org.apache.guacamole.form.Field;
import org.apache.guacamole.language.TranslatableGuacamoleClientException; import org.apache.guacamole.language.TranslatableGuacamoleClientException;
import org.apache.guacamole.language.TranslatableGuacamoleInsufficientCredentialsException; import org.apache.guacamole.language.TranslatableGuacamoleInsufficientCredentialsException;
import org.apache.guacamole.language.TranslatableMessage;
import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo; import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
@@ -38,6 +43,24 @@ import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
*/ */
public class UserVerificationService { public class UserVerificationService {
/**
* The name of the parameter which Duo will return in it's GET call-back
* that contains the code that the client will use to generate a token.
*/
private static final String DUO_CODE_PARAMETER_NAME = "duo_code";
/**
* The name of the parameter that will be used in the GET call-back that
* contains the session state.
*/
private static final String DUO_STATE_PARAMETER_NAME = "state";
/**
* The value that will be returned in the token if Duo authentication
* was successful.
*/
private static final String DUO_TOKEN_SUCCESS_VALUE = "ALLOW";
/** /**
* Service for retrieving Duo configuration information. * Service for retrieving Duo configuration information.
*/ */
@@ -45,10 +68,11 @@ public class UserVerificationService {
private ConfigurationService confService; private ConfigurationService confService;
/** /**
* Service for verifying users against Duo. * The authentication session manager that temporarily stores in-progress
* authentication attempts.
*/ */
@Inject @Inject
private DuoService duoService; private DuoAuthenticationSessionManager duoSessionManager;
/** /**
* Verifies the identity of the given user via the Duo multi-factor * Verifies the identity of the given user via the Duo multi-factor
@@ -75,39 +99,69 @@ public class UserVerificationService {
// Ignore anonymous users // Ignore anonymous users
if (authenticatedUser.getIdentifier().equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER)) if (authenticatedUser.getIdentifier().equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
return; return;
String username = authenticatedUser.getIdentifier();
// Retrieve signed Duo response from request try {
String signedResponse = request.getParameter(DuoSignedResponseField.PARAMETER_NAME);
// If no signed response, request one // Set up the Duo Client
if (signedResponse == null) { Client duoClient = new Client.Builder(
confService.getClientId(),
confService.getClientSecret(),
confService.getAPIHostname(),
confService.getRedirectUrl().toString())
.build();
duoClient.healthCheck();
// Retrieve signed Duo Code and State from the request
String duoCode = request.getParameter(DUO_CODE_PARAMETER_NAME);
String duoState = request.getParameter(DUO_STATE_PARAMETER_NAME);
// Create field which requests a signed response from Duo that // If no code or state is received, assume Duo MFA redirect has not occured and do it.
// verifies the identity of the given user via the configured if (duoCode == null || duoState == null) {
// Duo API endpoint
Field signedResponseField = new DuoSignedResponseField(
confService.getAPIHostname(),
duoService.createSignedRequest(authenticatedUser));
// Create an overall description of the additional credentials // Get a new session state from the Duo client
// required to verify identity duoState = duoClient.generateState();
CredentialsInfo expectedCredentials = new CredentialsInfo(
Collections.singletonList(signedResponseField)); // Add this session
duoSessionManager.defer(new DuoAuthenticationSession(confService.getAuthTimeout(), duoState, username), duoState);
// Request additional credentials // Request additional credentials
throw new TranslatableGuacamoleInsufficientCredentialsException( throw new TranslatableGuacamoleInsufficientCredentialsException(
"Verification using Duo is required before authentication " "Verification using Duo is required before authentication "
+ "can continue.", "LOGIN.INFO_DUO_AUTH_REQUIRED", + "can continue.", "LOGIN.INFO_DUO_AUTH_REQUIRED",
expectedCredentials); new CredentialsInfo(Collections.singletonList(
new RedirectField(
DUO_CODE_PARAMETER_NAME,
new URI(duoClient.createAuthUrl(username, duoState)),
new TranslatableMessage("LOGIN.INFO_DUO_REDIRECT_PENDING")
)
))
);
} }
// If signed response does not verify this user's identity, abort auth // Retrieve the deferred authenticaiton attempt
if (!duoService.isValidSignedResponse(authenticatedUser, signedResponse)) DuoAuthenticationSession duoSession = duoSessionManager.resume(duoState);
// Get the token from the DuoClient using the code and username, and check status
Token token = duoClient.exchangeAuthorizationCodeFor2FAResult(duoCode, duoSession.getUsername());
if (token == null
|| token.getAuth_result() == null
|| !DUO_TOKEN_SUCCESS_VALUE.equals(token.getAuth_result().getStatus()))
throw new TranslatableGuacamoleClientException("Provided Duo " throw new TranslatableGuacamoleClientException("Provided Duo "
+ "validation code is incorrect.", + "validation code is incorrect.",
"LOGIN.INFO_DUO_VALIDATION_CODE_INCORRECT"); "LOGIN.INFO_DUO_VALIDATION_CODE_INCORRECT");
}
catch (DuoException e) {
throw new GuacamoleServerException("Duo Client error.", e);
}
catch (URISyntaxException e) {
throw new GuacamoleServerException("Error creating URI from Duo Authentication URL.", e);
}
} }
} }

View File

@@ -1,245 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.duo.api;
import com.google.common.io.BaseEncoding;
import java.io.UnsupportedEncodingException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException;
/**
* Data which describes the identity of the user being verified by Duo.
*/
public class DuoCookie {
/**
* Pattern which matches valid cookies. Each cookie is made up of three
* sections, separated from each other by pipe symbols ("|").
*/
private static final Pattern COOKIE_FORMAT = Pattern.compile("([^|]+)\\|([^|]+)\\|([0-9]+)");
/**
* The index of the capturing group within COOKIE_FORMAT which contains the
* username.
*/
private static final int USERNAME_GROUP = 1;
/**
* The index of the capturing group within COOKIE_FORMAT which contains the
* integration key.
*/
private static final int INTEGRATION_KEY_GROUP = 2;
/**
* The index of the capturing group within COOKIE_FORMAT which contains the
* expiration timestamp.
*/
private static final int EXPIRATION_TIMESTAMP_GROUP = 3;
/**
* The username of the user being verified.
*/
private final String username;
/**
* The integration key provided by Duo and specific to this deployment of
* Guacamole.
*/
private final String integrationKey;
/**
* The time that this cookie expires, in seconds since midnight of
* 1970-01-01 (UTC).
*/
private final long expires;
/**
* Creates a new DuoCookie which describes the identity of a user being
* verified.
*
* @param username
* The username of the user being verified.
*
* @param integrationKey
* The integration key provided by Duo and specific to this deployment
* of Guacamole.
*
* @param expires
* The time that this cookie expires, in seconds since midnight of
* 1970-01-01 (UTC).
*/
public DuoCookie(String username, String integrationKey, long expires) {
this.username = username;
this.integrationKey = integrationKey;
this.expires = expires;
}
/**
* Returns the username of the user being verified.
*
* @return
* The username of the user being verified.
*/
public String getUsername() {
return username;
}
/**
* Returns the integration key provided by Duo and specific to this
* deployment of Guacamole.
*
* @return
* The integration key provided by Duo and specific to this deployment
* of Guacamole.
*/
public String getIntegrationKey() {
return integrationKey;
}
/**
* Returns the time that this cookie expires. The expiration time is
* represented in seconds since midnight of 1970-01-01 (UTC).
*
* @return
* The time that this cookie expires, in seconds since midnight of
* 1970-01-01 (UTC).
*/
public long getExpirationTimestamp(){
return expires;
}
/**
* Returns the current time as the number of seconds elapsed since
* midnight of 1970-01-01 (UTC).
*
* @return
* The current time as the number of seconds elapsed since midnight of
* 1970-01-01 (UTC).
*/
public static long currentTimestamp() {
return System.currentTimeMillis() / 1000;
}
/**
* Returns whether this cookie has expired (the current time has met or
* exceeded the expiration timestamp).
*
* @return
* true if this cookie has expired, false otherwise.
*/
public boolean isExpired() {
return currentTimestamp() >= expires;
}
/**
* Parses a base64-encoded Duo cookie, producing a new DuoCookie object
* containing the data therein. If the given string is not a valid Duo
* cookie, an exception is thrown. Note that the cookie may be expired, and
* must be checked for expiration prior to actual use.
*
* @param str
* The base64-encoded Duo cookie to parse.
*
* @return
* A new DuoCookie object containing the same data as the given
* base64-encoded Duo cookie string.
*
* @throws GuacamoleException
* If the given string is not a valid base64-encoded Duo cookie.
*/
public static DuoCookie parseDuoCookie(String str) throws GuacamoleException {
// Attempt to decode data as base64
String data;
try {
data = new String(BaseEncoding.base64().decode(str), "UTF-8");
}
// Bail if invalid base64 is provided
catch (IllegalArgumentException e) {
throw new GuacamoleClientException("Username is not correctly "
+ "encoded as base64.", e);
}
// Throw hard errors if standard pieces of Java are missing
catch (UnsupportedEncodingException e) {
throw new UnsupportedOperationException("Unexpected lack of "
+ "UTF-8 support.", e);
}
// Verify format of provided data
Matcher matcher = COOKIE_FORMAT.matcher(data);
if (!matcher.matches())
throw new GuacamoleClientException("Format of base64-encoded "
+ "username is invalid.");
// Get username and key (simple strings)
String username = matcher.group(USERNAME_GROUP);
String key = matcher.group(INTEGRATION_KEY_GROUP);
// Parse expiration time
long expires;
try {
expires = Long.parseLong(matcher.group(EXPIRATION_TIMESTAMP_GROUP));
}
// Bail if expiration timestamp is not a valid long
catch (NumberFormatException e) {
throw new GuacamoleClientException("Expiration timestamp is "
+ "not valid.", e);
}
// Return parsed cookie
return new DuoCookie(username, key, expires);
}
/**
* Returns the base64-encoded string representation of this DuoCookie. The
* format used is identical to that required by the Duo service: the
* username, integration key, and expiration timestamp separated by pipe
* symbols ("|") and encoded with base64.
*
* @return
* The base64-encoded string representation of this DuoCookie.
*/
@Override
public String toString() {
try {
// Separate each cookie field with pipe symbols
String data = username + "|" + integrationKey + "|" + expires;
// Encode resulting cookie string with base64
return BaseEncoding.base64().encode(data.getBytes("UTF-8"));
}
// Throw hard errors if standard pieces of Java are missing
catch (UnsupportedEncodingException e) {
throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
}
}
}

View File

@@ -1,205 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.duo.api;
import com.google.inject.Inject;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.duo.conf.ConfigurationService;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service which produces signed requests and parses/verifies signed responses
* as required by Duo's API.
*/
public class DuoService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(DuoService.class);
/**
* Pattern which matches valid Duo responses. Each response is made up of
* two sections, separated from each other by a colon, where each section
* is a signed Duo cookie.
*/
private static final Pattern RESPONSE_FORMAT = Pattern.compile("([^:]+):([^:]+)");
/**
* The index of the capturing group within RESPONSE_FORMAT which
* contains the DUO_RESPONSE cookie signed by the secret key.
*/
private static final int DUO_COOKIE_GROUP = 1;
/**
* The index of the capturing group within RESPONSE_FORMAT which
* contains the APPLICATION cookie signed by the application key.
*/
private static final int APP_COOKIE_GROUP = 2;
/**
* The amount of time that each generated cookie remains valid, in seconds.
*/
private static final int COOKIE_EXPIRATION_TIME = 300;
/**
* 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 prevents generation of the signature.
*/
public String createSignedRequest(AuthenticatedUser authenticatedUser)
throws GuacamoleException {
// Generate a cookie associating the username with the integration key
DuoCookie cookie = new DuoCookie(authenticatedUser.getIdentifier(),
confService.getIntegrationKey(),
DuoCookie.currentTimestamp() + COOKIE_EXPIRATION_TIME);
// Sign cookie with secret key
SignedDuoCookie duoCookie = new SignedDuoCookie(cookie,
SignedDuoCookie.Type.DUO_REQUEST,
confService.getSecretKey());
// Sign cookie with application key
SignedDuoCookie appCookie = new SignedDuoCookie(cookie,
SignedDuoCookie.Type.APPLICATION,
confService.getApplicationKey());
// Return signed request containing both signed cookies, separated by
// a colon (as required by Duo)
return duoCookie + ":" + appCookie;
}
/**
* 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 prevents validation of the signature.
*/
public boolean isValidSignedResponse(AuthenticatedUser authenticatedUser,
String signedResponse) throws GuacamoleException {
SignedDuoCookie duoCookie;
SignedDuoCookie appCookie;
// Retrieve username from externally-authenticated user
String username = authenticatedUser.getIdentifier();
// Retrieve Duo-specific keys from configuration
String applicationKey = confService.getApplicationKey();
String integrationKey = confService.getIntegrationKey();
String secretKey = confService.getSecretKey();
try {
// Verify format of response
Matcher matcher = RESPONSE_FORMAT.matcher(signedResponse);
if (!matcher.matches()) {
logger.debug("Duo response is not in correct format.");
return false;
}
// Parse signed cookie defining the user verified by Duo
duoCookie = SignedDuoCookie.parseSignedDuoCookie(secretKey,
matcher.group(DUO_COOKIE_GROUP));
// Parse signed cookie defining the user this application
// originally requested
appCookie = SignedDuoCookie.parseSignedDuoCookie(applicationKey,
matcher.group(APP_COOKIE_GROUP));
}
// Simply return false if signature fails to verify
catch (GuacamoleException e) {
logger.debug("Duo signature verification failed.", e);
return false;
}
// Verify neither cookie is expired
if (duoCookie.isExpired() || appCookie.isExpired()) {
logger.debug("Duo response contained expired cookie(s).");
return false;
}
// Verify the cookies in the response have the correct types
if (duoCookie.getType() != SignedDuoCookie.Type.DUO_RESPONSE
|| appCookie.getType() != SignedDuoCookie.Type.APPLICATION) {
logger.debug("Duo response did not contain correct cookie type(s).");
return false;
}
// Verify integration key matches both cookies
if (!duoCookie.getIntegrationKey().equals(integrationKey)
|| !appCookie.getIntegrationKey().equals(integrationKey)) {
logger.debug("Integration key of Duo response is incorrect.");
return false;
}
// Verify both cookies are for the current user
if (!duoCookie.getUsername().equals(username)
|| !appCookie.getUsername().equals(username)) {
logger.debug("Username of Duo response is incorrect.");
return false;
}
// All verifications tests pass
return true;
}
}

View File

@@ -1,332 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.duo.api;
import com.google.common.io.BaseEncoding;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
/**
* A DuoCookie which is cryptographically signed with a provided key using
* HMAC-SHA1.
*/
public class SignedDuoCookie extends DuoCookie {
/**
* Pattern which matches valid signed cookies. Like unsigned cookies, each
* signed cookie is made up of three sections, separated from each other by
* pipe symbols ("|").
*/
private static final Pattern SIGNED_COOKIE_FORMAT = Pattern.compile("([^|]+)\\|([^|]+)\\|([0-9a-f]+)");
/**
* The index of the capturing group within SIGNED_COOKIE_FORMAT which
* contains the cookie type prefix.
*/
private static final int PREFIX_GROUP = 1;
/**
* The index of the capturing group within SIGNED_COOKIE_FORMAT which
* contains the cookie's base64-encoded data.
*/
private static final int DATA_GROUP = 2;
/**
* The index of the capturing group within SIGNED_COOKIE_FORMAT which
* contains the signature.
*/
private static final int SIGNATURE_GROUP = 3;
/**
* The signature algorithm that should be used to sign the cookie, as
* defined by:
* http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Mac
*/
private static final String SIGNATURE_ALGORITHM = "HmacSHA1";
/**
* The type of a signed Duo cookie. Each signed Duo cookie has an
* associated type which determines the prefix included in the string
* representation of that cookie. As that type is included in the data
* that is signed, different types will result in different signatures,
* even if the data portion of the cookie is otherwise identical.
*/
public enum Type {
/**
* A Duo cookie which has been signed with the secret key for inclusion
* in a Duo request.
*/
DUO_REQUEST("TX"),
/**
* A Duo cookie which has been signed with the secret key by Duo and
* was included in a Duo response.
*/
DUO_RESPONSE("AUTH"),
/**
* A Duo cookie which has been signed with the application key for
* inclusion in a Duo request. Such cookies are also included in Duo
* responses, for verification by the application.
*/
APPLICATION("APP");
/**
* The prefix associated with the Duo cookie type. This prefix will
* be included in the string representation of the cookie.
*/
private final String prefix;
/**
* Creates a new Duo cookie type associated with the given string
* prefix. This prefix will be included in the string representation of
* the cookie.
*
* @param prefix
* The prefix to associated with the Duo cookie type.
*/
Type(String prefix) {
this.prefix = prefix;
}
/**
* Returns the prefix associated with the Duo cookie type.
*
* @return
* The prefix to associated with this Duo cookie type.
*/
public String getPrefix() {
return prefix;
}
/**
* Returns the cookie type associated with the given prefix. If no such
* cookie type exists, null is returned.
*
* @param prefix
* The prefix of the cookie type to search for.
*
* @return
* The cookie type associated with the given prefix, or null if no
* such cookie type exists.
*/
public static Type fromPrefix(String prefix) {
// Search through all defined cookie types for the given prefix
for (Type type : Type.values()) {
if (type.getPrefix().equals(prefix))
return type;
}
// No such cookie type exists
return null;
}
}
/**
* The type of this Duo cookie.
*/
private final Type type;
/**
* The signature produced when the cookie was signed with HMAC-SHA1. The
* signature covers the prefix of the type and the cookie's base64-encoded
* data, separated by a pipe symbol.
*/
private final String signature;
/**
* Creates a new SignedDuoCookie which describes the identity of a user
* being verified and is cryptographically signed with HMAC-SHA1 by a given
* key.
*
* @param cookie
* The cookie defining the identity being verified.
*
* @param type
* The type of the cookie being created.
*
* @param key
* The key to use to generate the cryptographic signature. This key
* will not be stored within the cookie.
*
* @throws GuacamoleException
* If the given signing key is invalid.
*/
public SignedDuoCookie(DuoCookie cookie, Type type, String key)
throws GuacamoleException {
// Init underlying cookie
super(cookie.getUsername(), cookie.getIntegrationKey(),
cookie.getExpirationTimestamp());
// Store cookie type and signature
this.type = type;
this.signature = sign(key, type.getPrefix() + "|" + cookie.toString());
}
/**
* Signs the given arbitrary string data with the given key using the
* algorithm defined by SIGNATURE_ALGORITHM. Both the data and the key will
* be interpreted as UTF-8 bytes.
*
* @param key
* The key which should be used to sign the given data.
*
* @param data
* The data being signed.
*
* @return
* The signature produced by signing the given data with the given key,
* encoded as lowercase hexadecimal.
*
* @throws GuacamoleException
* If the given signing key is invalid.
*/
private static String sign(String key, String data) throws GuacamoleException {
try {
// Attempt to sign UTF-8 bytes of provided data
Mac mac = Mac.getInstance(SIGNATURE_ALGORITHM);
mac.init(new SecretKeySpec(key.getBytes("UTF-8"), SIGNATURE_ALGORITHM));
// Return signature as hex
return BaseEncoding.base16().lowerCase().encode(mac.doFinal(data.getBytes("UTF-8")));
}
// Re-throw any errors which prevent signature
catch (InvalidKeyException e){
throw new GuacamoleServerException("Signing key is invalid.", e);
}
// Throw hard errors if standard pieces of Java are missing
catch (UnsupportedEncodingException e) {
throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
}
catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException("Unexpected lack of support "
+ "for required signature algorithm "
+ "\"" + SIGNATURE_ALGORITHM + "\".", e);
}
}
/**
* Returns the type of this Duo cookie. The Duo cookie type is dictated
* by the context of the cookie's use, and is included with the cookie's
* underlying data when generating the signature.
*
* @return
* The type of this Duo cookie.
*/
public Type getType() {
return type;
}
/**
* Returns the signature produced when the cookie was signed with HMAC-SHA1.
* The signature covers the prefix of the cookie's type and the cookie's
* base64-encoded data, separated by a pipe symbol.
*
* @return
* The signature produced when the cookie was signed with HMAC-SHA1.
*/
public String getSignature() {
return signature;
}
/**
* Parses a signed Duo cookie string, such as that produced by the
* toString() function or received from the Duo service, producing a new
* SignedDuoCookie object containing the associated cookie data and
* signature. If the given string is not a valid Duo cookie, or if the
* signature is incorrect, an exception is thrown. Note that the cookie may
* be expired, and must be checked for expiration prior to actual use.
*
* @param key
* The key that was used to sign the Duo cookie.
*
* @param str
* The Duo cookie string to parse.
*
* @return
* A new SignedDuoCookie object containing the same data and signature
* as the given Duo cookie string.
*
* @throws GuacamoleException
* If the given string is not a valid Duo cookie string, or if the
* signature of the cookie is invalid.
*/
public static SignedDuoCookie parseSignedDuoCookie(String key, String str)
throws GuacamoleException {
// Verify format of provided data
Matcher matcher = SIGNED_COOKIE_FORMAT.matcher(str);
if (!matcher.matches())
throw new GuacamoleClientException("Format of signed Duo cookie "
+ "is invalid.");
// Parse type from prefix
Type type = Type.fromPrefix(matcher.group(PREFIX_GROUP));
if (type == null)
throw new GuacamoleClientException("Invalid Duo cookie prefix.");
// Parse cookie from base64-encoded data
DuoCookie cookie = DuoCookie.parseDuoCookie(matcher.group(DATA_GROUP));
// Verify signature of cookie
SignedDuoCookie signedCookie = new SignedDuoCookie(cookie, type, key);
if (!signedCookie.getSignature().equals(matcher.group(SIGNATURE_GROUP)))
throw new GuacamoleClientException("Duo cookie has incorrect signature.");
// Cookie has valid signature and has parsed successfully
return signedCookie;
}
/**
* Returns the string representation of this SignedDuoCookie. The format
* used is identical to that required by the Duo service: the type prefix,
* base64-encoded cookie data, and HMAC-SHA1 signature separated by pipe
* symbols ("|").
*
* @return
* The string representation of this SignedDuoCookie.
*/
@Override
public String toString() {
return type.getPrefix() + "|" + super.toString() + "|" + signature;
}
}

View File

@@ -20,9 +20,12 @@
package org.apache.guacamole.auth.duo.conf; package org.apache.guacamole.auth.duo.conf;
import com.google.inject.Inject; import com.google.inject.Inject;
import java.net.URI;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment; import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
import org.apache.guacamole.properties.StringGuacamoleProperty; import org.apache.guacamole.properties.StringGuacamoleProperty;
import org.apache.guacamole.properties.URIGuacamoleProperty;
/** /**
* Service for retrieving configuration information regarding the Duo * Service for retrieving configuration information regarding the Duo
@@ -56,11 +59,11 @@ public class ConfigurationService {
* key received from Duo for verifying Guacamole users. This value MUST be * key received from Duo for verifying Guacamole users. This value MUST be
* exactly 20 characters. * exactly 20 characters.
*/ */
private static final StringGuacamoleProperty DUO_INTEGRATION_KEY = private static final StringGuacamoleProperty DUO_CLIENT_ID =
new StringGuacamoleProperty() { new StringGuacamoleProperty() {
@Override @Override
public String getName() { return "duo-integration-key"; } public String getName() { return "duo-client-id"; }
}; };
@@ -69,26 +72,38 @@ public class ConfigurationService {
* received from Duo for verifying Guacamole users. This value MUST be * received from Duo for verifying Guacamole users. This value MUST be
* exactly 40 characters. * exactly 40 characters.
*/ */
private static final StringGuacamoleProperty DUO_SECRET_KEY = private static final StringGuacamoleProperty DUO_CLIENT_SECRET =
new StringGuacamoleProperty() { new StringGuacamoleProperty() {
@Override @Override
public String getName() { return "duo-secret-key"; } public String getName() { return "duo-client-secret"; }
}; };
/** /**
* The property within guacamole.properties which defines the arbitrary * The property within guacamole.properties which defines the redirect URL
* random key which was generated for Guacamole. Note that this value is not * that Duo will call after the second factor has been completed. This
* provided by Duo, but is expected to be generated by the administrator of * should be the URL used to access Guacamole.
* the system hosting Guacamole. This value MUST be at least 40 characters.
*/ */
private static final StringGuacamoleProperty DUO_APPLICATION_KEY = private static final URIGuacamoleProperty DUO_REDIRECT_URL =
new StringGuacamoleProperty() { new URIGuacamoleProperty() {
@Override @Override
public String getName() { return "duo-application-key"; } public String getName() { return "duo-redirect-url"; }
};
/**
* The property that configures the timeout, in seconds, of in-progress
* Duo authentication attempts. Authentication attempts that take longer
* than this period of time will be invalidated.
*/
private static final IntegerGuacamoleProperty DUO_AUTH_TIMEOUT =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "duo-auth-timeout"; }
}; };
/** /**
@@ -110,51 +125,65 @@ public class ConfigurationService {
} }
/** /**
* Returns the integration key received from Duo for verifying Guacamole * Returns the Duo client id received from Duo for verifying Guacamole
* users, as defined in guacamole.properties by the "duo-integration-key" * users, as defined in guacamole.properties by the "duo-client-id"
* property. This value MUST be exactly 20 characters. * property. This value MUST be exactly 20 characters.
* *
* @return * @return
* The integration key received from Duo for verifying Guacamole * The client id received from Duo for verifying Guacamole users.
* users.
* *
* @throws GuacamoleException * @throws GuacamoleException
* If the associated property within guacamole.properties is missing. * If the associated property within guacamole.properties is missing.
*/ */
public String getIntegrationKey() throws GuacamoleException { public String getClientId() throws GuacamoleException {
return environment.getRequiredProperty(DUO_INTEGRATION_KEY); return environment.getRequiredProperty(DUO_CLIENT_ID);
} }
/** /**
* Returns the secret key received from Duo for verifying Guacamole users, * Returns the client secert received from Duo for verifying Guacamole users,
* as defined in guacamole.properties by the "duo-secret-key" property. This * as defined in guacamole.properties by the "duo-client-secert" property.
* value MUST be exactly 20 characters. * This value MUST be exactly 20 characters.
* *
* @return * @return
* The secret key received from Duo for verifying Guacamole users. * The client secret received from Duo for verifying Guacamole users.
* *
* @throws GuacamoleException * @throws GuacamoleException
* If the associated property within guacamole.properties is missing. * If the associated property within guacamole.properties is missing.
*/ */
public String getSecretKey() throws GuacamoleException { public String getClientSecret() throws GuacamoleException {
return environment.getRequiredProperty(DUO_SECRET_KEY); return environment.getRequiredProperty(DUO_CLIENT_SECRET);
} }
/** /**
* Returns the arbitrary random key which was generated for Guacamole, as * Return the callback URL that will be called by Duo after authentication
* defined in guacamole.properties by the "duo-application-key" property. * with Duo has been completed. This should be the URL to return the user
* Note that this value is not provided by Duo, but is expected to be * to the Guacamole interface, and will be a full URL.
* generated by the administrator of the system hosting Guacamole. This *
* value MUST be at least 40 characters.
*
* @return * @return
* The arbitrary random key which was generated for Guacamole. * The URL for Duo to use to callback to the Guacamole interface after
* * authentication has been completed.
* @throws GuacamoleException *
* If the associated property within guacamole.properties is missing. * @throws GuacamoleException
* If guacamole.properties cannot be read, or if the property is not
* defined.
*/ */
public String getApplicationKey() throws GuacamoleException { public URI getRedirectUrl() throws GuacamoleException {
return environment.getRequiredProperty(DUO_APPLICATION_KEY); return environment.getRequiredProperty(DUO_REDIRECT_URL);
}
/**
* Return the number of seconds after which in-progress authentication attempts with
* Duo should be invalidated. The default is 30 seconds.
*
* @return
* The number of seconds after which in-progress Duo MFA attempts should
* be invalidated.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public int getAuthTimeout() throws GuacamoleException {
return environment.getProperty(DUO_AUTH_TIMEOUT, 30);
} }
} }

View File

@@ -1,98 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.duo.form;
import org.apache.guacamole.form.Field;
/**
* 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.
*/
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;
}
}

View File

@@ -1,33 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* 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'
});
}]);

View File

@@ -1,86 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* 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', '$element',
function duoSignedResponseController($scope, $element) {
/**
* The iframe which contains the Duo authentication interface.
*
* @type HTMLIFrameElement
*/
var iframe = $element.find('iframe')[0];
/**
* The submit button which should be used to submit the login form once
* the Duo response has been received.
*
* @type HTMLInputElement
*/
var submit = $element.find('input[type="submit"]')[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
submit.click();
};
// 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
});
}]);

View File

@@ -20,18 +20,6 @@
"translations/pt.json", "translations/pt.json",
"translations/ru.json", "translations/ru.json",
"translations/zh.json" "translations/zh.json"
], ]
"js" : [
"duo.min.js"
],
"css" : [
"duo.min.css"
],
"resources" : {
"templates/duoSignedResponseField.html" : "text/html"
}
} }

View File

@@ -1,366 +0,0 @@
/**
* 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));

View File

@@ -1,27 +0,0 @@
/*
* 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.
*/

View File

@@ -1,62 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.duo-signature-response-field-container {
height: 100%;
width: 100%;
position: fixed;
left: 0;
top: 0;
display: table;
background: white;
}
.duo-signature-response-field {
width: 100%;
display: table-cell;
vertical-align: middle;
}
.duo-signature-response-field input[type="submit"] {
display: none !important;
}
.duo-signature-response-field iframe {
width: 100%;
max-width: 620px;
height: 330px;
border: none;
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);
display: block;
margin: 1.5em auto;
}
.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;
}

View File

@@ -1,6 +0,0 @@
<div class="duo-signature-response-field-container">
<div class="duo-signature-response-field" ng-class="{ loading : !duoInterfaceLoaded }">
<iframe></iframe>
<input type="submit">
</div>
</div>