mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
GUACAMOLE-1289: Update the Duo extension to the v4 API
This commit is contained in:
committed by
Alex Leitner
parent
13494baa4a
commit
e8860e4dd8
@@ -39,93 +39,9 @@
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<enforcer.skip>true</enforcer.skip>
|
||||
</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>
|
||||
|
||||
<!-- Guacamole Extension API -->
|
||||
@@ -156,6 +72,20 @@
|
||||
<scope>provided</scope>
|
||||
</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>
|
||||
|
||||
</project>
|
||||
|
@@ -21,7 +21,6 @@ package org.apache.guacamole.auth.duo;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
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.environment.Environment;
|
||||
import org.apache.guacamole.environment.LocalEnvironment;
|
||||
@@ -74,8 +73,8 @@ public class DuoAuthenticationProviderModule extends AbstractModule {
|
||||
|
||||
// Bind Duo-specific services
|
||||
bind(ConfigurationService.class);
|
||||
bind(DuoService.class);
|
||||
bind(UserVerificationService.class);
|
||||
bind(DuoAuthenticationSessionManager.class);
|
||||
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -17,12 +17,18 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module which provides handling for Duo multi-factor authentication.
|
||||
*/
|
||||
angular.module('guacDuo', [
|
||||
'form'
|
||||
]);
|
||||
package org.apache.guacamole.auth.duo;
|
||||
|
||||
// Ensure the guacDuo module is loaded along with the rest of the app
|
||||
angular.module('index').requires.push('guacDuo');
|
||||
import com.google.inject.Singleton;
|
||||
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.
|
||||
|
||||
}
|
@@ -19,16 +19,21 @@
|
||||
|
||||
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 java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collections;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
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.form.DuoSignedResponseField;
|
||||
import org.apache.guacamole.form.Field;
|
||||
import org.apache.guacamole.form.RedirectField;
|
||||
import org.apache.guacamole.language.TranslatableGuacamoleClientException;
|
||||
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.Credentials;
|
||||
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
|
||||
@@ -38,6 +43,24 @@ import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -45,10 +68,11 @@ public class UserVerificationService {
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Service for verifying users against Duo.
|
||||
* The authentication session manager that temporarily stores in-progress
|
||||
* authentication attempts.
|
||||
*/
|
||||
@Inject
|
||||
private DuoService duoService;
|
||||
private DuoAuthenticationSessionManager duoSessionManager;
|
||||
|
||||
/**
|
||||
* Verifies the identity of the given user via the Duo multi-factor
|
||||
@@ -76,38 +100,68 @@ public class UserVerificationService {
|
||||
if (authenticatedUser.getIdentifier().equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
|
||||
return;
|
||||
|
||||
// Retrieve signed Duo response from request
|
||||
String signedResponse = request.getParameter(DuoSignedResponseField.PARAMETER_NAME);
|
||||
String username = authenticatedUser.getIdentifier();
|
||||
|
||||
// If no signed response, request one
|
||||
if (signedResponse == null) {
|
||||
try {
|
||||
|
||||
// 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(
|
||||
// Set up the Duo Client
|
||||
Client duoClient = new Client.Builder(
|
||||
confService.getClientId(),
|
||||
confService.getClientSecret(),
|
||||
confService.getAPIHostname(),
|
||||
duoService.createSignedRequest(authenticatedUser));
|
||||
confService.getRedirectUrl().toString())
|
||||
.build();
|
||||
|
||||
// Create an overall description of the additional credentials
|
||||
// required to verify identity
|
||||
CredentialsInfo expectedCredentials = new CredentialsInfo(
|
||||
Collections.singletonList(signedResponseField));
|
||||
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);
|
||||
|
||||
// If no code or state is received, assume Duo MFA redirect has not occured and do it.
|
||||
if (duoCode == null || duoState == null) {
|
||||
|
||||
// Get a new session state from the Duo client
|
||||
duoState = duoClient.generateState();
|
||||
|
||||
// Add this session
|
||||
duoSessionManager.defer(new DuoAuthenticationSession(confService.getAuthTimeout(), duoState, username), duoState);
|
||||
|
||||
// Request additional credentials
|
||||
throw new TranslatableGuacamoleInsufficientCredentialsException(
|
||||
"Verification using Duo is required before authentication "
|
||||
+ "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
|
||||
if (!duoService.isValidSignedResponse(authenticatedUser, signedResponse))
|
||||
// Retrieve the deferred authenticaiton attempt
|
||||
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 "
|
||||
+ "validation code is 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -20,9 +20,12 @@
|
||||
package org.apache.guacamole.auth.duo.conf;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import java.net.URI;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.environment.Environment;
|
||||
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
|
||||
import org.apache.guacamole.properties.StringGuacamoleProperty;
|
||||
import org.apache.guacamole.properties.URIGuacamoleProperty;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* exactly 20 characters.
|
||||
*/
|
||||
private static final StringGuacamoleProperty DUO_INTEGRATION_KEY =
|
||||
private static final StringGuacamoleProperty DUO_CLIENT_ID =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "duo-integration-key"; }
|
||||
public String getName() { return "duo-client-id"; }
|
||||
|
||||
};
|
||||
|
||||
@@ -69,25 +72,37 @@ public class ConfigurationService {
|
||||
* received from Duo for verifying Guacamole users. This value MUST be
|
||||
* exactly 40 characters.
|
||||
*/
|
||||
private static final StringGuacamoleProperty DUO_SECRET_KEY =
|
||||
private static final StringGuacamoleProperty DUO_CLIENT_SECRET =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "duo-secret-key"; }
|
||||
public String getName() { return "duo-client-secret"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* The property within guacamole.properties which defines the redirect URL
|
||||
* that Duo will call after the second factor has been completed. This
|
||||
* should be the URL used to access Guacamole.
|
||||
*/
|
||||
private static final StringGuacamoleProperty DUO_APPLICATION_KEY =
|
||||
new StringGuacamoleProperty() {
|
||||
private static final URIGuacamoleProperty DUO_REDIRECT_URL =
|
||||
new URIGuacamoleProperty() {
|
||||
|
||||
@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
|
||||
* users, as defined in guacamole.properties by the "duo-integration-key"
|
||||
* Returns the Duo client id received from Duo for verifying Guacamole
|
||||
* users, as defined in guacamole.properties by the "duo-client-id"
|
||||
* property. This value MUST be exactly 20 characters.
|
||||
*
|
||||
* @return
|
||||
* The integration key received from Duo for verifying Guacamole
|
||||
* users.
|
||||
* The client id 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);
|
||||
public String getClientId() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(DUO_CLIENT_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Returns the client secert received from Duo for verifying Guacamole users,
|
||||
* as defined in guacamole.properties by the "duo-client-secert" property.
|
||||
* This value MUST be exactly 20 characters.
|
||||
*
|
||||
* @return
|
||||
* The secret key received from Duo for verifying Guacamole users.
|
||||
* The client secret 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);
|
||||
public String getClientSecret() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(DUO_CLIENT_SECRET);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 callback URL that will be called by Duo after authentication
|
||||
* with Duo has been completed. This should be the URL to return the user
|
||||
* to the Guacamole interface, and will be a full URL.
|
||||
*
|
||||
* @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.
|
||||
* If guacamole.properties cannot be read, or if the property is not
|
||||
* defined.
|
||||
*/
|
||||
public String getApplicationKey() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(DUO_APPLICATION_KEY);
|
||||
public URI getRedirectUrl() throws GuacamoleException {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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'
|
||||
});
|
||||
|
||||
}]);
|
@@ -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
|
||||
});
|
||||
|
||||
}]);
|
@@ -20,18 +20,6 @@
|
||||
"translations/pt.json",
|
||||
"translations/ru.json",
|
||||
"translations/zh.json"
|
||||
],
|
||||
|
||||
"js" : [
|
||||
"duo.min.js"
|
||||
],
|
||||
|
||||
"css" : [
|
||||
"duo.min.css"
|
||||
],
|
||||
|
||||
"resources" : {
|
||||
"templates/duoSignedResponseField.html" : "text/html"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
|
@@ -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));
|
@@ -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.
|
||||
*/
|
@@ -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;
|
||||
}
|
@@ -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>
|
Reference in New Issue
Block a user