Skip to content

Commit

Permalink
GUACAMOLE-1289: Update the Duo extension to the v4 API
Browse files Browse the repository at this point in the history
  • Loading branch information
necouchman committed Jan 31, 2024
1 parent cdcbf6a commit 20bfbe6
Show file tree
Hide file tree
Showing 17 changed files with 254 additions and 1,634 deletions.
100 changes: 15 additions & 85 deletions extensions/guacamole-auth-duo/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->
Expand Down Expand Up @@ -155,6 +71,20 @@
<version>2.5</version>
<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>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

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;
Expand Down Expand Up @@ -74,8 +73,8 @@ protected void configure() {

// Bind Duo-specific services
bind(ConfigurationService.class);
bind(DuoService.class);
bind(UserVerificationService.class);
bind(DuoAuthenticationSessionManager.class);

}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,18 @@
* under the License.
*/

package org.apache.guacamole.auth.duo;

import com.google.inject.Singleton;
import org.apache.guacamole.net.auth.AuthenticationSessionManager;

/**
* Module which provides handling for Duo multi-factor authentication.
* An AuthenticationSessionManager implementation that temporarily stores
* authentication attempts for Duo MFA while they are underway.
*/
angular.module('guacDuo', [
'form'
]);

// Ensure the guacDuo module is loaded along with the rest of the app
angular.module('index').requires.push('guacDuo');
@Singleton
public class DuoAuthenticationSessionManager extends AuthenticationSessionManager<DuoAuthenticationSession> {
// Nothing to see here.
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,17 +43,36 @@
*/
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.
*/
@Inject
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
Expand All @@ -75,39 +99,69 @@ public void verifyAuthenticatedUser(AuthenticatedUser authenticatedUser)
// Ignore anonymous users
if (authenticatedUser.getIdentifier().equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
return;

// Retrieve signed Duo response from request
String signedResponse = request.getParameter(DuoSignedResponseField.PARAMETER_NAME);

// If no signed response, request one
if (signedResponse == null) {

// Create field which requests a signed response from Duo that
// verifies the identity of the given user via the configured
// Duo API endpoint
Field signedResponseField = new DuoSignedResponseField(
confService.getAPIHostname(),
duoService.createSignedRequest(authenticatedUser));

// Create an overall description of the additional credentials
// required to verify identity
CredentialsInfo expectedCredentials = new CredentialsInfo(
Collections.singletonList(signedResponseField));

String username = authenticatedUser.getIdentifier();

try {

// Set up the Duo Client
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);

// 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);
"Verification using Duo is required before authentication "
+ "can continue.", "LOGIN.INFO_DUO_AUTH_REQUIRED",
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);
}
}

}
Loading

0 comments on commit 20bfbe6

Please sign in to comment.