GUACAMOLE-1364: Refactor all SSO extensions beneath common base.

This commit is contained in:
Michael Jumper
2021-11-25 17:54:08 -08:00
parent ea657099f5
commit 36a02c1f90
86 changed files with 326 additions and 62 deletions

View File

@@ -0,0 +1,2 @@
target/
*~

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-base</artifactId>
<packaging>jar</packaging>
<name>guacamole-auth-sso-base</name>
<url>http://guacamole.apache.org/</url>
<parent>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso</artifactId>
<version>1.3.0</version>
<relativePath>../../</relativePath>
</parent>
</project>

View File

@@ -0,0 +1,3 @@
*~
target/
src/main/resources/generated/

View File

@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-cas</artifactId>
<packaging>jar</packaging>
<version>1.3.0</version>
<name>guacamole-auth-sso-cas</name>
<url>http://guacamole.apache.org/</url>
<parent>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso</artifactId>
<version>1.3.0</version>
<relativePath>../../</relativePath>
</parent>
<dependencies>
<!-- Guacamole Extension API -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
</dependency>
<!-- Core SSO support -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-base</artifactId>
</dependency>
<!-- Apereo CAS Client API -->
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.6.2</version>
</dependency>
<!-- Guava - Utility Library -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!-- Guice -->
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
</dependency>
<!-- Java servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</dependency>
<!-- JAX-RS Annotations -->
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
<id>dist</id>
<baseDirectory>${project.artifactId}-${project.version}</baseDirectory>
<!-- Output tar.gz -->
<formats>
<format>tar.gz</format>
</formats>
<!-- Include licenses and extension .jar -->
<fileSets>
<!-- Include licenses -->
<fileSet>
<outputDirectory></outputDirectory>
<directory>target/licenses</directory>
</fileSet>
<!-- Include extension .jar -->
<fileSet>
<directory>target</directory>
<outputDirectory></outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,98 @@
/*
* 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.cas;
import com.google.inject.Inject;
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.form.Field;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.apache.guacamole.auth.cas.conf.ConfigurationService;
import org.apache.guacamole.auth.cas.form.CASTicketField;
import org.apache.guacamole.auth.cas.ticket.TicketValidationService;
import org.apache.guacamole.auth.cas.user.CASAuthenticatedUser;
import org.apache.guacamole.language.TranslatableMessage;
/**
* Service providing convenience functions for the CAS AuthenticationProvider
* implementation.
*/
public class AuthenticationProviderService {
/**
* Service for retrieving CAS configuration information.
*/
@Inject
private ConfigurationService confService;
/**
* Service for validating received ID tickets.
*/
@Inject
private TicketValidationService ticketService;
/**
* Returns an AuthenticatedUser representing the user authenticated by the
* given credentials.
*
* @param credentials
* The credentials to use for authentication.
*
* @return
* A CASAuthenticatedUser representing the user authenticated by the
* given credentials.
*
* @throws GuacamoleException
* If an error occurs while authenticating the user, or if access is
* denied.
*/
public CASAuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
// Pull CAS ticket from request if present
HttpServletRequest request = credentials.getRequest();
if (request != null) {
String ticket = request.getParameter(CASTicketField.PARAMETER_NAME);
if (ticket != null) {
return ticketService.validateTicket(ticket, credentials);
}
}
// Request CAS ticket
throw new GuacamoleInvalidCredentialsException("Invalid login.",
new CredentialsInfo(Arrays.asList(new Field[] {
// CAS-specific ticket (will automatically redirect the user
// to the authorization page via JavaScript)
new CASTicketField(
confService.getAuthorizationEndpoint(),
confService.getRedirectURI(),
new TranslatableMessage("LOGIN.INFO_CAS_REDIRECT_PENDING")
)
}))
);
}
}

View File

@@ -0,0 +1,90 @@
/*
* 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.cas;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.cas.user.CASAuthenticatedUser;
import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.TokenInjectingUserContext;
import org.apache.guacamole.net.auth.UserContext;
/**
* Guacamole authentication backend which authenticates users using an
* arbitrary external system implementing CAS. No storage for connections is
* provided - only authentication. Storage must be provided by some other
* extension.
*/
public class CASAuthenticationProvider extends AbstractAuthenticationProvider {
/**
* Injector which will manage the object graph of this authentication
* provider.
*/
private final Injector injector;
/**
* Creates a new CASAuthenticationProvider that authenticates users
* against an CAS service
*
* @throws GuacamoleException
* If a required property is missing, or an error occurs while parsing
* a property.
*/
public CASAuthenticationProvider() throws GuacamoleException {
// Set up Guice injector.
injector = Guice.createInjector(
new CASAuthenticationProviderModule(this)
);
}
@Override
public String getIdentifier() {
return "cas";
}
@Override
public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
// Attempt to authenticate user with given credentials
AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class);
return authProviderService.authenticateUser(credentials);
}
@Override
public UserContext decorate(UserContext context,
AuthenticatedUser authenticatedUser, Credentials credentials)
throws GuacamoleException {
if (!(authenticatedUser instanceof CASAuthenticatedUser))
return context;
return new TokenInjectingUserContext(context,
((CASAuthenticatedUser) authenticatedUser).getTokens());
}
}

View File

@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.cas;
import org.apache.guacamole.auth.cas.conf.ConfigurationService;
import com.google.inject.AbstractModule;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.auth.cas.ticket.TicketValidationService;
/**
* Guice module which configures CAS-specific injections.
*/
public class CASAuthenticationProviderModule extends AbstractModule {
/**
* Guacamole server environment.
*/
private final Environment environment;
/**
* A reference to the CASAuthenticationProvider on behalf of which this
* module has configured injection.
*/
private final AuthenticationProvider authProvider;
/**
* Creates a new CAS authentication provider module which configures
* injection for the CASAuthenticationProvider.
*
* @param authProvider
* The AuthenticationProvider for which injection is being configured.
*
* @throws GuacamoleException
* If an error occurs while retrieving the Guacamole server
* environment.
*/
public CASAuthenticationProviderModule(AuthenticationProvider authProvider)
throws GuacamoleException {
// Get local environment
this.environment = LocalEnvironment.getInstance();
// Store associated auth provider
this.authProvider = authProvider;
}
@Override
protected void configure() {
// Bind core implementations of guacamole-ext classes
bind(AuthenticationProvider.class).toInstance(authProvider);
bind(Environment.class).toInstance(environment);
// Bind CAS-specific services
bind(ConfigurationService.class);
bind(TicketValidationService.class);
}
}

View File

@@ -0,0 +1,121 @@
/*
* 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.cas.conf;
import org.apache.guacamole.auth.cas.group.GroupFormat;
import org.apache.guacamole.properties.EnumGuacamoleProperty;
import org.apache.guacamole.properties.URIGuacamoleProperty;
import org.apache.guacamole.properties.StringGuacamoleProperty;
/**
* Provides properties required for use of the CAS authentication provider.
* These properties will be read from guacamole.properties when the CAS
* authentication provider is used.
*/
public class CASGuacamoleProperties {
/**
* This class should not be instantiated.
*/
private CASGuacamoleProperties() {}
/**
* The authorization endpoint (URI) of the CAS service.
*/
public static final URIGuacamoleProperty CAS_AUTHORIZATION_ENDPOINT =
new URIGuacamoleProperty() {
@Override
public String getName() { return "cas-authorization-endpoint"; }
};
/**
* The URI that the CAS service should redirect to after the
* authentication process is complete. This must be the full URL that a
* user would enter into their browser to access Guacamole.
*/
public static final URIGuacamoleProperty CAS_REDIRECT_URI =
new URIGuacamoleProperty() {
@Override
public String getName() { return "cas-redirect-uri"; }
};
/**
* The location of the private key file used to retrieve the
* password if CAS is configured to support ClearPass.
*/
public static final PrivateKeyGuacamoleProperty CAS_CLEARPASS_KEY =
new PrivateKeyGuacamoleProperty() {
@Override
public String getName() { return "cas-clearpass-key"; }
};
/**
* The name of the CAS attribute used for group membership, such as
* "memberOf". This attribute is case sensitive.
*/
public static final StringGuacamoleProperty CAS_GROUP_ATTRIBUTE =
new StringGuacamoleProperty() {
@Override
public String getName() { return "cas-group-attribute"; }
};
/**
* The format used by CAS to represent group names. Possible formats are
* "plain" (simple text names) or "ldap" (fully-qualified LDAP DNs).
*/
public static final EnumGuacamoleProperty<GroupFormat> CAS_GROUP_FORMAT =
new EnumGuacamoleProperty<GroupFormat>(GroupFormat.class) {
@Override
public String getName() { return "cas-group-format"; }
};
/**
* The LDAP base DN to require for all CAS groups.
*/
public static final LdapNameGuacamoleProperty CAS_GROUP_LDAP_BASE_DN =
new LdapNameGuacamoleProperty() {
@Override
public String getName() { return "cas-group-ldap-base-dn"; }
};
/**
* The LDAP attribute to require for the names of CAS groups.
*/
public static final StringGuacamoleProperty CAS_GROUP_LDAP_ATTRIBUTE =
new StringGuacamoleProperty() {
@Override
public String getName() { return "cas-group-ldap-attribute"; }
};
}

View File

@@ -0,0 +1,192 @@
/*
* 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.cas.conf;
import com.google.inject.Inject;
import java.net.URI;
import java.security.PrivateKey;
import javax.naming.ldap.LdapName;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.cas.group.GroupFormat;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.auth.cas.group.GroupParser;
import org.apache.guacamole.auth.cas.group.LDAPGroupParser;
import org.apache.guacamole.auth.cas.group.PlainGroupParser;
/**
* Service for retrieving configuration information regarding the CAS service.
*/
public class ConfigurationService {
/**
* The Guacamole server environment.
*/
@Inject
private Environment environment;
/**
* Returns the authorization endpoint (URI) of the CAS service as
* configured with guacamole.properties.
*
* @return
* The authorization endpoint of the CAS service, as configured with
* guacamole.properties.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or if the authorization
* endpoint property is missing.
*/
public URI getAuthorizationEndpoint() throws GuacamoleException {
return environment.getRequiredProperty(CASGuacamoleProperties.CAS_AUTHORIZATION_ENDPOINT);
}
/**
* Returns the URI that the CAS service should redirect to after
* the authentication process is complete, as configured with
* guacamole.properties. This must be the full URL that a user would enter
* into their browser to access Guacamole.
*
* @return
* The URI to redirect the client back to after authentication
* is completed, as configured in guacamole.properties.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or if the redirect URI
* property is missing.
*/
public URI getRedirectURI() throws GuacamoleException {
return environment.getRequiredProperty(CASGuacamoleProperties.CAS_REDIRECT_URI);
}
/**
* Returns the PrivateKey used to decrypt the credential object
* sent encrypted by CAS, or null if no key is defined.
*
* @return
* The PrivateKey used to decrypt the ClearPass
* credential returned by CAS.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public PrivateKey getClearpassKey() throws GuacamoleException {
return environment.getProperty(CASGuacamoleProperties.CAS_CLEARPASS_KEY);
}
/**
* Returns the CAS attribute that should be used to determine group
* memberships in CAS, such as "memberOf". If no attribute has been
* specified, null is returned.
*
* @return
* The attribute name used to determine group memberships in CAS,
* null if not defined.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getGroupAttribute() throws GuacamoleException {
return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_ATTRIBUTE);
}
/**
* Returns the format that CAS is expected to use for its group names, such
* as {@link GroupFormat#PLAIN} (simple plain-text names) or
* {@link GroupFormat#LDAP} (fully-qualified LDAP DNs). If not specified,
* PLAIN is used by default.
*
* @return
* The format that CAS is expected to use for its group names.
*
* @throws GuacamoleException
* If the format specified within guacamole.properties is not valid.
*/
public GroupFormat getGroupFormat() throws GuacamoleException {
return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_FORMAT, GroupFormat.PLAIN);
}
/**
* Returns the base DN that all LDAP-formatted CAS groups must reside
* beneath. Any groups that are not beneath this base DN should be ignored.
* If no such base DN is provided, the tree structure of the ancestors of
* LDAP-formatted CAS groups should not be considered.
*
* @return
* The base DN that all LDAP-formatted CAS groups must reside beneath,
* or null if the tree structure of the ancestors of LDAP-formatted
* CAS groups should not be considered.
*
* @throws GuacamoleException
* If the provided base DN is not a valid LDAP DN.
*/
public LdapName getGroupLDAPBaseDN() throws GuacamoleException {
return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_LDAP_BASE_DN);
}
/**
* Returns the LDAP attribute that should be required for all LDAP-formatted
* CAS groups. Any groups that do not use this attribute as the last
* (leftmost) attribute of their DN should be ignored. If no such LDAP
* attribute is provided, the last (leftmost) attribute should still be
* used to determine the group name, but the specific attribute involved
* should not be considered.
*
* @return
* The LDAP attribute that should be required for all LDAP-formatted
* CAS groups, or null if any attribute should be allowed.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getGroupLDAPAttribute() throws GuacamoleException {
return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_LDAP_ATTRIBUTE);
}
/**
* Returns a GroupParser instance that can be used to parse CAS group
* names. The parser returned will take into account the configured CAS
* group format, as well as any configured LDAP-specific restrictions.
*
* @return
* A GroupParser instance that can be used to parse CAS group names as
* configured in guacamole.properties.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public GroupParser getGroupParser() throws GuacamoleException {
switch (getGroupFormat()) {
// Simple, plain-text groups
case PLAIN:
return new PlainGroupParser();
// LDAP DNs
case LDAP:
return new LDAPGroupParser(getGroupLDAPAttribute(), getGroupLDAPBaseDN());
default:
throw new GuacamoleServerException("Unsupported CAS group format: " + getGroupFormat());
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.cas.conf;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import org.apache.guacamole.properties.GuacamoleProperty;
import org.apache.guacamole.GuacamoleServerException;
/**
* A GuacamoleProperty whose value is an LDAP DN.
*/
public abstract class LdapNameGuacamoleProperty implements GuacamoleProperty<LdapName> {
@Override
public LdapName parseValue(String value) throws GuacamoleServerException {
// Consider null/empty values to be empty
if (value == null || value.isEmpty())
return null;
// Parse provided value as an LDAP DN
try {
return new LdapName(value);
}
catch (InvalidNameException e) {
throw new GuacamoleServerException("Invalid LDAP distinguished name.", e);
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.cas.conf;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import org.apache.guacamole.properties.GuacamoleProperty;
import org.apache.guacamole.GuacamoleServerException;
/**
* A GuacamoleProperty whose value is derived from a private key file.
*/
public abstract class PrivateKeyGuacamoleProperty implements GuacamoleProperty<PrivateKey> {
@Override
public PrivateKey parseValue(String value) throws GuacamoleServerException {
if (value == null || value.isEmpty())
return null;
FileInputStream keyStreamIn = null;
try {
try {
// Open and read the file specified in the configuration.
File keyFile = new File(value);
keyStreamIn = new FileInputStream(keyFile);
ByteArrayOutputStream keyStreamOut = new ByteArrayOutputStream();
byte[] keyBuffer = new byte[1024];
for (int readBytes; (readBytes = keyStreamIn.read(keyBuffer)) != -1;)
keyStreamOut.write(keyBuffer, 0, readBytes);
final byte[] keyBytes = keyStreamOut.toByteArray();
// Set up decryption infrastructure
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
KeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
return keyFactory.generatePrivate(keySpec);
}
catch (FileNotFoundException e) {
throw new GuacamoleServerException("Could not find the specified key file.", e);
}
catch (NoSuchAlgorithmException e) {
throw new GuacamoleServerException("RSA algorithm is not available.", e);
}
catch (InvalidKeySpecException e) {
throw new GuacamoleServerException("Key is not in expected PKCS8 encoding.", e);
}
finally {
if (keyStreamIn != null)
keyStreamIn.close();
}
}
catch (IOException e) {
throw new GuacamoleServerException("Could not read in the specified key file.", e);
}
}
}

View File

@@ -0,0 +1,78 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.cas.form;
import java.net.URI;
import javax.ws.rs.core.UriBuilder;
import org.apache.guacamole.form.RedirectField;
import org.apache.guacamole.language.TranslatableMessage;
/**
* Field definition which represents the ticket returned by an CAS service.
* This is processed transparently - the user is redirected to CAS, authenticates
* and then is returned to Guacamole where the ticket field is
* processed.
*/
public class CASTicketField extends RedirectField {
/**
* The parameter that will be present upon successful CAS authentication.
*/
public static final String PARAMETER_NAME = "ticket";
/**
* The standard URI name for the CAS login resource.
*/
private static final String CAS_LOGIN_URI = "login";
/**
* Creates a new CAS "ticket" field which links to the given CAS
* service using the provided client ID. Successful authentication at the
* CAS service will result in the client being redirected to the specified
* redirect URI. The CAS ticket will be embedded in the fragment (the part
* following the hash symbol) of that URI, which the JavaScript side of
* this extension will move to the query parameters.
*
* @param authorizationEndpoint
* The full URL of the endpoint accepting CAS authentication
* requests.
*
* @param redirectURI
* The URI that the CAS service should redirect to upon successful
* authentication.
*
* @param redirectMessage
* The message that will be displayed for the user while the redirect
* is processed. This will be processed through Guacamole's translation
* system.
*/
public CASTicketField(URI authorizationEndpoint, URI redirectURI,
TranslatableMessage redirectMessage) {
super(PARAMETER_NAME, UriBuilder.fromUri(authorizationEndpoint)
.path(CAS_LOGIN_URI)
.queryParam("service", redirectURI)
.build(),
redirectMessage);
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.cas.group;
import org.apache.guacamole.properties.EnumGuacamoleProperty.PropertyValue;
/**
* Possible formats of group names received from CAS.
*/
public enum GroupFormat {
/**
* Simple, plain-text group names.
*/
@PropertyValue("plain")
PLAIN,
/**
* Group names formatted as LDAP DNs.
*/
@PropertyValue("ldap")
LDAP
}

View File

@@ -0,0 +1,44 @@
/*
* 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.cas.group;
/**
* Parser which converts the group names returned by CAS into names usable by
* Guacamole. The format of a CAS group name may vary by the underlying
* authentication backend. For example, a CAS deployment backed by LDAP may
* provide group names as LDAP DNs, which must be transformed into normal group
* names to be usable within Guacamole.
*
* @see LDAPGroupParser
*/
public interface GroupParser {
/**
* Parses the given CAS group name into a group name usable by Guacamole.
*
* @param casGroup
* The group name retrieved from CAS.
*
* @return
* A group name usable by Guacamole, or null if the group is not valid.
*/
String parse(String casGroup);
}

View File

@@ -0,0 +1,106 @@
/*
* 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.cas.group;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* GroupParser that converts group names from LDAP DNs into normal group names,
* using the last (leftmost) attribute of the DN as the name. Groups may
* optionally be restricted to only those beneath a specific base DN, or only
* those using a specific attribute as their last (leftmost) attribute.
*/
public class LDAPGroupParser implements GroupParser {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(LDAPGroupParser.class);
/**
* The LDAP attribute to require for all accepted group names. If null, any
* LDAP attribute will be allowed.
*/
private final String nameAttribute;
/**
* The base DN to require for all accepted group names. If null, ancestor
* tree structure will not be considered in accepting/rejecting a group.
*/
private final LdapName baseDn;
/**
* Creates a new LDAPGroupParser which applies the given restrictions on
* any provided group names.
*
* @param nameAttribute
* The LDAP attribute to require for all accepted group names. This
* restriction applies to the last (leftmost) attribute only, which is
* always used to determine the name of the group. If null, any LDAP
* attribute will be allowed in the last (leftmost) position.
*
* @param baseDn
* The base DN to require for all accepted group names. If null,
* ancestor tree structure will not be considered in
* accepting/rejecting a group.
*/
public LDAPGroupParser(String nameAttribute, LdapName baseDn) {
this.nameAttribute = nameAttribute;
this.baseDn = baseDn;
}
@Override
public String parse(String casGroup) {
// Reject null/empty group names
if (casGroup == null || casGroup.isEmpty())
return null;
// Parse group as an LDAP DN
LdapName group;
try {
group = new LdapName(casGroup);
}
catch (InvalidNameException e) {
logger.debug("CAS group \"{}\" has been rejected as it is not a "
+ "valid LDAP DN.", casGroup, e);
return null;
}
// Reject any group that is not beneath the base DN
if (baseDn != null && !group.startsWith(baseDn))
return null;
// If a specific name attribute is defined, restrict to groups that
// use that attribute to distinguish themselves
Rdn last = group.getRdn(group.size() - 1);
if (nameAttribute != null && !nameAttribute.equalsIgnoreCase(last.getType()))
return null;
// The group name is the string value of the final attribute in the DN
return last.getValue().toString();
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.cas.group;
/**
* GroupParser which simply passes through all CAS group names untouched.
*/
public class PlainGroupParser implements GroupParser {
@Override
public String parse(String casGroup) {
return casGroup;
}
}

View File

@@ -0,0 +1,271 @@
/*
* 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.cas.ticket;
import com.google.common.io.BaseEncoding;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.cas.conf.ConfigurationService;
import org.apache.guacamole.auth.cas.user.CASAuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.token.TokenName;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.Cas20ProxyTicketValidator;
import org.jasig.cas.client.validation.TicketValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service for validating ID tickets forwarded to us by the client, verifying
* that they did indeed come from the CAS service.
*/
public class TicketValidationService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(TicketValidationService.class);
/**
* The prefix to use when generating token names.
*/
public static final String CAS_ATTRIBUTE_TOKEN_PREFIX = "CAS_";
/**
* Service for retrieving CAS configuration information.
*/
@Inject
private ConfigurationService confService;
/**
* Provider for AuthenticatedUser objects.
*/
@Inject
private Provider<CASAuthenticatedUser> authenticatedUserProvider;
/**
* Converts the given CAS attribute value object (whose type is variable)
* to a Set of String values. If the value is already a Collection of some
* kind, its values are converted to Strings and returned as the members of
* the Set. If the value is not already a Collection, it is assumed to be a
* single value, converted to a String, and used as the sole member of the
* set.
*
* @param obj
* The CAS attribute value to convert to a Set of Strings.
*
* @return
* A Set of all String values contained within the given CAS attribute
* value.
*/
private Set<String> toStringSet(Object obj) {
// Consider null to represent no provided values
if (obj == null)
return Collections.emptySet();
// If the provided object is already a Collection, produce a Collection
// where we know for certain that all values are Strings
if (obj instanceof Collection) {
return ((Collection<?>) obj).stream()
.map(Object::toString)
.collect(Collectors.toSet());
}
// Otherwise, assume we have only a single value
return Collections.singleton(obj.toString());
}
/**
* Validates and parses the given ID ticket, returning a map of all
* available tokens for the given user based on attributes provided by the
* CAS server. If the ticket is invalid an exception is thrown.
*
* @param ticket
* The ID ticket to validate and parse.
*
* @param credentials
* The Credentials object to store retrieved username and
* password values in.
*
* @return
* A CASAuthenticatedUser instance containing the ticket data returned by the CAS server.
*
* @throws GuacamoleException
* If the ID ticket is not valid or guacamole.properties could
* not be parsed.
*/
public CASAuthenticatedUser validateTicket(String ticket,
Credentials credentials) throws GuacamoleException {
// Create a ticket validator that uses the configured CAS URL
URI casServerUrl = confService.getAuthorizationEndpoint();
Cas20ProxyTicketValidator validator = new Cas20ProxyTicketValidator(casServerUrl.toString());
validator.setAcceptAnyProxy(true);
validator.setEncoding("UTF-8");
// Attempt to validate the supplied ticket
Assertion assertion;
try {
URI confRedirectURI = confService.getRedirectURI();
assertion = validator.validate(ticket, confRedirectURI.toString());
}
catch (TicketValidationException e) {
throw new GuacamoleException("Ticket validation failed.", e);
}
// Pull user principal and associated attributes
AttributePrincipal principal = assertion.getPrincipal();
Map<String, Object> ticketAttrs = new HashMap<>(principal.getAttributes());
// Retrieve user identity from principal
String username = principal.getName();
if (username == null)
throw new GuacamoleSecurityException("No username provided by CAS.");
// Update credentials with username provided by CAS for sake of
// ${GUAC_USERNAME} token
credentials.setUsername(username);
// Retrieve password, attempt decryption, and set credentials.
Object credObj = ticketAttrs.remove("credential");
if (credObj != null) {
String clearPass = decryptPassword(credObj.toString());
if (clearPass != null && !clearPass.isEmpty())
credentials.setPassword(clearPass);
}
Set<String> effectiveGroups;
// Parse effective groups from principal attributes if a specific
// group attribute has been configured
String groupAttribute = confService.getGroupAttribute();
if (groupAttribute != null) {
effectiveGroups = toStringSet(ticketAttrs.get(groupAttribute)).stream()
.map(confService.getGroupParser()::parse)
.collect(Collectors.toSet());
}
// Otherwise, assume no effective groups
else
effectiveGroups = Collections.emptySet();
// Convert remaining attributes that have values to Strings
Map<String, String> tokens = new HashMap<>(ticketAttrs.size());
ticketAttrs.forEach((key, value) -> {
if (value != null) {
String tokenName = TokenName.canonicalize(key, CAS_ATTRIBUTE_TOKEN_PREFIX);
tokens.put(tokenName, value.toString());
}
});
CASAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
authenticatedUser.init(username, credentials, tokens, effectiveGroups);
return authenticatedUser;
}
/**
* Takes an encrypted string representing a password provided by
* the CAS ClearPass service and decrypts it using the private
* key configured for this extension. Returns null if it is
* unable to decrypt the password.
*
* @param encryptedPassword
* A string with the encrypted password provided by the
* CAS service.
*
* @return
* The decrypted password, or null if it is unable to
* decrypt the password.
*
* @throws GuacamoleException
* If unable to get Guacamole configuration data
*/
private final String decryptPassword(String encryptedPassword)
throws GuacamoleException {
// If we get nothing, we return nothing.
if (encryptedPassword == null || encryptedPassword.isEmpty()) {
logger.warn("No or empty encrypted password, no password will be available.");
return null;
}
final PrivateKey clearpassKey = confService.getClearpassKey();
if (clearpassKey == null) {
logger.debug("No private key available to decrypt password.");
return null;
}
try {
final Cipher cipher = Cipher.getInstance(clearpassKey.getAlgorithm());
if (cipher == null)
throw new GuacamoleServerException("Failed to initialize cipher object with private key.");
// Initialize the Cipher in decrypt mode.
cipher.init(Cipher.DECRYPT_MODE, clearpassKey);
// Decode and decrypt, and return a new string.
final byte[] pass64 = BaseEncoding.base64().decode(encryptedPassword);
final byte[] cipherData = cipher.doFinal(pass64);
return new String(cipherData, Charset.forName("UTF-8"));
}
catch (BadPaddingException e) {
throw new GuacamoleServerException("Bad padding when decrypting cipher data.", e);
}
catch (IllegalBlockSizeException e) {
throw new GuacamoleServerException("Illegal block size while opening private key.", e);
}
catch (InvalidKeyException e) {
throw new GuacamoleServerException("Specified private key for ClearPass decryption is invalid.", e);
}
catch (NoSuchAlgorithmException e) {
throw new GuacamoleServerException("Unexpected algorithm for the private key.", e);
}
catch (NoSuchPaddingException e) {
throw new GuacamoleServerException("No such padding trying to initialize cipher with private key.", e);
}
}
}

View File

@@ -0,0 +1,122 @@
/*
* 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.cas.user;
import com.google.inject.Inject;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import org.apache.guacamole.net.auth.AbstractAuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
/**
* An CAS-specific implementation of AuthenticatedUser, associating a
* username and particular set of credentials with the CAS authentication
* provider.
*/
public class CASAuthenticatedUser extends AbstractAuthenticatedUser {
/**
* Reference to the authentication provider associated with this
* authenticated user.
*/
@Inject
private AuthenticationProvider authProvider;
/**
* The credentials provided when this user was authenticated.
*/
private Credentials credentials;
/**
* Tokens associated with this authenticated user.
*/
private Map<String, String> tokens;
/**
* The unique identifiers of all user groups which this user is a member of.
*/
private Set<String> effectiveGroups;
/**
* Initializes this AuthenticatedUser using the given username and
* credentials, and an empty map of parameter tokens.
*
* @param username
* The username of the user that was authenticated.
*
* @param credentials
* The credentials provided when this user was authenticated.
*/
public void init(String username, Credentials credentials) {
this.init(username, credentials, Collections.emptyMap(), Collections.emptySet());
}
/**
* Initializes this AuthenticatedUser using the given username,
* credentials, and parameter tokens.
*
* @param username
* The username of the user that was authenticated.
*
* @param credentials
* The credentials provided when this user was authenticated.
*
* @param tokens
* A map of all the name/value pairs that should be available
* as tokens when connections are established with this user.
*/
public void init(String username, Credentials credentials,
Map<String, String> tokens, Set<String> effectiveGroups) {
this.credentials = credentials;
this.tokens = Collections.unmodifiableMap(tokens);
this.effectiveGroups = effectiveGroups;
setIdentifier(username.toLowerCase());
}
/**
* Returns a Map containing the name/value pairs that can be applied
* as parameter tokens when connections are established by the user.
*
* @return
* A Map containing all of the name/value pairs that can be
* used as parameter tokens by this user.
*/
public Map<String, String> getTokens() {
return tokens;
}
@Override
public AuthenticationProvider getAuthenticationProvider() {
return authProvider;
}
@Override
public Credentials getCredentials() {
return credentials;
}
@Override
public Set<String> getEffectiveUserGroups() {
return effectiveGroups;
}
}

View File

@@ -0,0 +1,23 @@
{
"guacamoleVersion" : "1.3.0",
"name" : "CAS Authentication Extension",
"namespace" : "cas",
"authProviders" : [
"org.apache.guacamole.auth.cas.CASAuthenticationProvider"
],
"translations" : [
"translations/ca.json",
"translations/de.json",
"translations/en.json",
"translations/fr.json",
"translations/ja.json",
"translations/ko.json",
"translations/pt.json",
"translations/ru.json",
"translations/zh.json"
]
}

View File

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

View File

@@ -0,0 +1,11 @@
{
"DATA_SOURCE_CAS" : {
"NAME" : "Backend d'inici de sessió unificat (SSO) CAS"
},
"LOGIN" : {
"INFO_CAS_REDIRECT_PENDING" : "Espereu, redireccionant a l'autenticació CAS ..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_CAS" : {
"NAME" : "CAS SSO Backend"
},
"LOGIN" : {
"FIELD_HEADER_TICKET" : "",
"INFO_CAS_REDIRECT_PENDING" : "Bitte warten, Sie werden zur CAS-Authentifizierung weitergeleitet..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_CAS" : {
"NAME" : "CAS SSO Backend"
},
"LOGIN" : {
"FIELD_HEADER_TICKET" : "",
"INFO_CAS_REDIRECT_PENDING" : "Please wait, redirecting to CAS authentication..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_CAS" : {
"NAME" : "CAS SSO Backend"
},
"LOGIN" : {
"FIELD_HEADER_TICKET" : "",
"INFO_CAS_REDIRECT_PENDING" : "Veuillez patienter, redirection vers l'authentification CAS..."
}
}

View File

@@ -0,0 +1,7 @@
{
"LOGIN" : {
"INFO_CAS_REDIRECT_PENDING" : "CAS認証にリダイレクトしています。"
}
}

View File

@@ -0,0 +1,7 @@
{
"LOGIN" : {
"INFO_CAS_REDIRECT_PENDING" : "기다려주십시오. CAS 인증으로 리디렉션 중..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_CAS" : {
"NAME" : "CAS SSO Backend"
},
"LOGIN" : {
"FIELD_HEADER_TICKET" : "",
"INFO_CAS_REDIRECT_PENDING" : "Por favor aguarde, redirecionando para autenticação CAS..."
}
}

View File

@@ -0,0 +1,11 @@
{
"DATA_SOURCE_CAS" : {
"NAME" : "Бэкенд CAS SSO"
},
"LOGIN" : {
"INFO_CAS_REDIRECT_PENDING" : "Пожалуйста, подождите. Переадресую на страницу аутентификации CAS..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_CAS" : {
"NAME" : "CAS SSO后端"
},
"LOGIN" : {
"FIELD_HEADER_TICKET" : "",
"INFO_CAS_REDIRECT_PENDING" : "请稍候正在重定向到CAS验证..."
}
}

View File

@@ -0,0 +1,164 @@
/*
* 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.cas.group;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
/**
* Test which confirms that the LDAPGroupParser implementation of GroupParser
* parses CAS groups correctly.
*/
public class LDAPGroupParserTest {
/**
* LdapName instance representing the LDAP DN: "dc=example,dc=net".
*/
private final LdapName exampleBaseDn;
/**
* Creates a new LDAPGroupParserTest that verifies the functionality of
* LDAPGroupParser.
*
* @throws InvalidNameException
* If the static string LDAP DN of any test instance of LdapName is
* unexpectedly invalid.
*/
public LDAPGroupParserTest() throws InvalidNameException {
exampleBaseDn = new LdapName("dc=example,dc=net");
}
/**
* Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
* when no restrictions are enforced on LDAP attributes or the base DN.
*/
@Test
public void testParseRestrictNothing() {
GroupParser parser = new LDAPGroupParser(null, null);
// null input should be rejected as null
assertNull(parser.parse(null));
// Invalid DNs should be rejected as null
assertNull(parser.parse(""));
assertNull(parser.parse("foo"));
// Valid DNs should be accepted
assertEquals("bar", parser.parse("foo=bar"));
assertEquals("baz", parser.parse("CN=baz,dc=example,dc=com"));
assertEquals("baz", parser.parse("ou=baz,dc=example,dc=net"));
assertEquals("foo", parser.parse("ou=foo,cn=baz,dc=example,dc=net"));
assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
}
/**
* Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
* when restrictions are enforced on LDAP attributes only.
*/
@Test
public void testParseRestrictAttribute() {
GroupParser parser = new LDAPGroupParser("cn", null);
// null input should be rejected as null
assertNull(parser.parse(null));
// Invalid DNs should be rejected as null
assertNull(parser.parse(""));
assertNull(parser.parse("foo"));
// Valid DNs not using the correct attribute should be rejected as null
assertNull(parser.parse("foo=bar"));
assertNull(parser.parse("ou=baz,dc=example,dc=com"));
assertNull(parser.parse("ou=foo,cn=baz,dc=example,dc=com"));
// Valid DNs using the correct attribute should be accepted
assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
assertEquals("baz", parser.parse("CN=baz,dc=example,dc=com"));
}
/**
* Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
* when restrictions are enforced on the LDAP base DN only.
*/
@Test
public void testParseRestrictBaseDN() {
GroupParser parser = new LDAPGroupParser(null, exampleBaseDn);
// null input should be rejected as null
assertNull(parser.parse(null));
// Invalid DNs should be rejected as null
assertNull(parser.parse(""));
assertNull(parser.parse("foo"));
// Valid DNs outside the base DN should be rejected as null
assertNull(parser.parse("foo=bar"));
assertNull(parser.parse("CN=baz,dc=example,dc=com"));
// Valid DNs beneath the base DN should be accepted
assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
assertEquals("baz", parser.parse("ou=baz,dc=example,dc=net"));
assertEquals("foo", parser.parse("ou=foo,cn=baz,dc=example,dc=net"));
}
/**
* Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
* when restrictions are enforced on both LDAP attributes and the base DN.
*/
@Test
public void testParseRestrictAll() {
GroupParser parser = new LDAPGroupParser("cn", exampleBaseDn);
// null input should be rejected as null
assertNull(parser.parse(null));
// Invalid DNs should be rejected as null
assertNull(parser.parse(""));
assertNull(parser.parse("foo"));
// Valid DNs outside the base DN should be rejected as null
assertNull(parser.parse("foo=bar"));
assertNull(parser.parse("CN=baz,dc=example,dc=com"));
// Valid DNs beneath the base DN but not using the correct attribute
// should be rejected as null
assertNull(parser.parse("ou=baz,dc=example,dc=net"));
assertNull(parser.parse("ou=foo,cn=baz,dc=example,dc=net"));
// Valid DNs beneath the base DN and using the correct attribute should
// be accepted
assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
}
}

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-dist</artifactId>
<packaging>pom</packaging>
<name>guacamole-auth-sso-dist</name>
<url>http://guacamole.apache.org/</url>
<parent>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso</artifactId>
<version>1.3.0</version>
<relativePath>../../</relativePath>
</parent>
<dependencies>
<!-- CAS Authentication Extension -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-cas</artifactId>
<version>1.3.0</version>
</dependency>
<!-- OpenID Authentication Extension -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-openid</artifactId>
<version>1.3.0</version>
</dependency>
<!-- SAML Authentication Extension -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-saml</artifactId>
<version>1.3.0</version>
</dependency>
</dependencies>
<build>
<!-- Dist .tar.gz for guacamole-auth-sso should be named after the
parent guacamole-auth-sso project, not after guacamole-auth-sso-dist -->
<finalName>${project.parent.artifactId}-${project.parent.version}</finalName>
</build>
</project>

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
<id>dist</id>
<baseDirectory>${project.parent.artifactId}-${project.parent.version}</baseDirectory>
<!-- Output .tar.gz -->
<formats>
<format>tar.gz</format>
</formats>
<!-- Include extension .jars -->
<dependencySets>
<!-- CAS extension .jar -->
<dependencySet>
<outputDirectory>cas</outputDirectory>
<useProjectArtifact>false</useProjectArtifact>
<includes>
<include>org.apache.guacamole:guacamole-auth-sso-cas</include>
</includes>
</dependencySet>
<!-- OpenID extension .jar -->
<dependencySet>
<outputDirectory>openid</outputDirectory>
<useProjectArtifact>false</useProjectArtifact>
<includes>
<include>org.apache.guacamole:guacamole-auth-sso-openid</include>
</includes>
</dependencySet>
<!-- SAML extension .jar -->
<dependencySet>
<outputDirectory>saml</outputDirectory>
<useProjectArtifact>false</useProjectArtifact>
<includes>
<include>org.apache.guacamole:guacamole-auth-sso-saml</include>
</includes>
</dependencySet>
</dependencySets>
<!-- Include extension licenses -->
<fileSets>
<fileSet>
<outputDirectory></outputDirectory>
<directory>target/licenses</directory>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,3 @@
*~
target/
src/main/resources/generated/

View File

@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-openid</artifactId>
<packaging>jar</packaging>
<version>1.3.0</version>
<name>guacamole-auth-sso-openid</name>
<url>http://guacamole.apache.org/</url>
<parent>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso</artifactId>
<version>1.3.0</version>
<relativePath>../../</relativePath>
</parent>
<build>
<plugins>
<!-- 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>
<jsSourceDir>/</jsSourceDir>
<jsTargetDir>/</jsTargetDir>
<jsFinalFile>openid.js</jsFinalFile>
<jsSourceFiles>
<jsSourceFile>license.txt</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 -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
</dependency>
<!-- Core SSO support -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-base</artifactId>
</dependency>
<!-- Java implementation of JOSE (jose.4.j) -->
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.7.6</version>
</dependency>
<!-- Guice -->
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
</dependency>
<!-- Java servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</dependency>
<!-- JAX-RS Annotations -->
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
<id>dist</id>
<baseDirectory>${project.artifactId}-${project.version}</baseDirectory>
<!-- Output tar.gz -->
<formats>
<format>tar.gz</format>
</formats>
<!-- Include licenses and extension .jar -->
<fileSets>
<!-- Include licenses -->
<fileSet>
<outputDirectory></outputDirectory>
<directory>target/licenses</directory>
</fileSet>
<!-- Include extension .jar -->
<fileSet>
<directory>target</directory>
<outputDirectory></outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,142 @@
/*
* 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.openid;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.Arrays;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.auth.openid.conf.ConfigurationService;
import org.apache.guacamole.auth.openid.form.TokenField;
import org.apache.guacamole.auth.openid.token.NonceService;
import org.apache.guacamole.auth.openid.token.TokenValidationService;
import org.apache.guacamole.auth.openid.user.AuthenticatedUser;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.form.Field;
import org.apache.guacamole.language.TranslatableMessage;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.jose4j.jwt.JwtClaims;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service providing convenience functions for the OpenID AuthenticationProvider
* implementation.
*/
public class AuthenticationProviderService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class);
/**
* Service for retrieving OpenID configuration information.
*/
@Inject
private ConfigurationService confService;
/**
* Service for validating and generating unique nonce values.
*/
@Inject
private NonceService nonceService;
/**
* Service for validating received ID tokens.
*/
@Inject
private TokenValidationService tokenService;
/**
* Provider for AuthenticatedUser objects.
*/
@Inject
private Provider<AuthenticatedUser> authenticatedUserProvider;
/**
* Returns an AuthenticatedUser representing the user authenticated by the
* given credentials.
*
* @param credentials
* The credentials to use for authentication.
*
* @return
* An AuthenticatedUser representing the user authenticated by the
* given credentials.
*
* @throws GuacamoleException
* If an error occurs while authenticating the user, or if access is
* denied.
*/
public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
String username = null;
Set<String> groups = null;
// Validate OpenID token in request, if present, and derive username
HttpServletRequest request = credentials.getRequest();
if (request != null) {
String token = request.getParameter(TokenField.PARAMETER_NAME);
if (token != null) {
JwtClaims claims = tokenService.validateToken(token);
if (claims != null) {
username = tokenService.processUsername(claims);
groups = tokenService.processGroups(claims);
}
}
}
// If the username was successfully retrieved from the token, produce
// authenticated user
if (username != null) {
// Create corresponding authenticated user
AuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
authenticatedUser.init(username, credentials, groups);
return authenticatedUser;
}
// Request OpenID token
throw new GuacamoleInvalidCredentialsException("Invalid login.",
new CredentialsInfo(Arrays.asList(new Field[] {
// OpenID-specific token (will automatically redirect the user
// to the authorization page via JavaScript)
new TokenField(
confService.getAuthorizationEndpoint(),
confService.getScope(),
confService.getClientID(),
confService.getRedirectURI(),
nonceService.generate(confService.getMaxNonceValidity() * 60000L),
new TranslatableMessage("LOGIN.INFO_OID_REDIRECT_PENDING")
)
}))
);
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.openid;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials;
/**
* Guacamole authentication backend which authenticates users using an
* arbitrary external system implementing OpenID. No storage for connections is
* provided - only authentication. Storage must be provided by some other
* extension.
*/
public class OpenIDAuthenticationProvider extends AbstractAuthenticationProvider {
/**
* Injector which will manage the object graph of this authentication
* provider.
*/
private final Injector injector;
/**
* Creates a new OpenIDAuthenticationProvider that authenticates users
* against an OpenID service.
*
* @throws GuacamoleException
* If a required property is missing, or an error occurs while parsing
* a property.
*/
public OpenIDAuthenticationProvider() throws GuacamoleException {
// Set up Guice injector.
injector = Guice.createInjector(
new OpenIDAuthenticationProviderModule(this)
);
}
@Override
public String getIdentifier() {
return "openid";
}
@Override
public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
// Attempt to authenticate user with given credentials
AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class);
return authProviderService.authenticateUser(credentials);
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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.openid;
import com.google.inject.AbstractModule;
import org.apache.guacamole.auth.openid.conf.ConfigurationService;
import org.apache.guacamole.auth.openid.token.NonceService;
import org.apache.guacamole.auth.openid.token.TokenValidationService;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.net.auth.AuthenticationProvider;
/**
* Guice module which configures openid-specific injections.
*/
public class OpenIDAuthenticationProviderModule extends AbstractModule {
/**
* Guacamole server environment.
*/
private final Environment environment;
/**
* A reference to the OpenIDAuthenticationProvider on behalf of which this
* module has configured injection.
*/
private final AuthenticationProvider authProvider;
/**
* Creates a new OpenID authentication provider module which configures
* injection for the OpenIDAuthenticationProvider.
*
* @param authProvider
* The AuthenticationProvider for which injection is being configured.
*
* @throws GuacamoleException
* If an error occurs while retrieving the Guacamole server
* environment.
*/
public OpenIDAuthenticationProviderModule(AuthenticationProvider authProvider)
throws GuacamoleException {
// Get local environment
this.environment = LocalEnvironment.getInstance();
// Store associated auth provider
this.authProvider = authProvider;
}
@Override
protected void configure() {
// Bind core implementations of guacamole-ext classes
bind(AuthenticationProvider.class).toInstance(authProvider);
bind(Environment.class).toInstance(environment);
// Bind openid-specific services
bind(ConfigurationService.class);
bind(NonceService.class);
bind(TokenValidationService.class);
}
}

View File

@@ -0,0 +1,398 @@
/*
* 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.openid.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 OpenID
* service.
*/
public class ConfigurationService {
/**
* The default claim type to use to retrieve an authenticated user's
* username.
*/
private static final String DEFAULT_USERNAME_CLAIM_TYPE = "email";
/**
* The default claim type to use to retrieve an authenticated user's
* groups.
*/
private static final String DEFAULT_GROUPS_CLAIM_TYPE = "groups";
/**
* The default space-separated list of OpenID scopes to request.
*/
private static final String DEFAULT_SCOPE = "openid email profile";
/**
* The default amount of clock skew tolerated for timestamp comparisons
* between the Guacamole server and OpenID service clocks, in seconds.
*/
private static final int DEFAULT_ALLOWED_CLOCK_SKEW = 30;
/**
* The default maximum amount of time that an OpenID token should remain
* valid, in minutes.
*/
private static final int DEFAULT_MAX_TOKEN_VALIDITY = 300;
/**
* The default maximum amount of time that a nonce generated by the
* Guacamole server should remain valid, in minutes.
*/
private static final int DEFAULT_MAX_NONCE_VALIDITY = 10;
/**
* The authorization endpoint (URI) of the OpenID service.
*/
private static final URIGuacamoleProperty OPENID_AUTHORIZATION_ENDPOINT =
new URIGuacamoleProperty() {
@Override
public String getName() { return "openid-authorization-endpoint"; }
};
/**
* The endpoint (URI) of the JWKS service which defines how received ID
* tokens (JWTs) shall be validated.
*/
private static final URIGuacamoleProperty OPENID_JWKS_ENDPOINT =
new URIGuacamoleProperty() {
@Override
public String getName() { return "openid-jwks-endpoint"; }
};
/**
* The issuer to expect for all received ID tokens.
*/
private static final StringGuacamoleProperty OPENID_ISSUER =
new StringGuacamoleProperty() {
@Override
public String getName() { return "openid-issuer"; }
};
/**
* The claim type which contains the authenticated user's username within
* any valid JWT.
*/
private static final StringGuacamoleProperty OPENID_USERNAME_CLAIM_TYPE =
new StringGuacamoleProperty() {
@Override
public String getName() { return "openid-username-claim-type"; }
};
/**
* The claim type which contains the authenticated user's groups within
* any valid JWT.
*/
private static final StringGuacamoleProperty OPENID_GROUPS_CLAIM_TYPE =
new StringGuacamoleProperty() {
@Override
public String getName() { return "openid-groups-claim-type"; }
};
/**
* The space-separated list of OpenID scopes to request.
*/
private static final StringGuacamoleProperty OPENID_SCOPE =
new StringGuacamoleProperty() {
@Override
public String getName() { return "openid-scope"; }
};
/**
* The amount of clock skew tolerated for timestamp comparisons between the
* Guacamole server and OpenID service clocks, in seconds.
*/
private static final IntegerGuacamoleProperty OPENID_ALLOWED_CLOCK_SKEW =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "openid-allowed-clock-skew"; }
};
/**
* The maximum amount of time that an OpenID token should remain valid, in
* minutes.
*/
private static final IntegerGuacamoleProperty OPENID_MAX_TOKEN_VALIDITY =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "openid-max-token-validity"; }
};
/**
* The maximum amount of time that a nonce generated by the Guacamole server
* should remain valid, in minutes. As each OpenID request has a unique
* nonce value, this imposes an upper limit on the amount of time any
* particular OpenID request can result in successful authentication within
* Guacamole.
*/
private static final IntegerGuacamoleProperty OPENID_MAX_NONCE_VALIDITY =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "openid-max-nonce-validity"; }
};
/**
* OpenID client ID which should be submitted to the OpenID service when
* necessary. This value is typically provided by the OpenID service when
* OpenID credentials are generated for your application.
*/
private static final StringGuacamoleProperty OPENID_CLIENT_ID =
new StringGuacamoleProperty() {
@Override
public String getName() { return "openid-client-id"; }
};
/**
* The URI that the OpenID service should redirect to after the
* authentication process is complete. This must be the full URL that a
* user would enter into their browser to access Guacamole.
*/
private static final URIGuacamoleProperty OPENID_REDIRECT_URI =
new URIGuacamoleProperty() {
@Override
public String getName() { return "openid-redirect-uri"; }
};
/**
* The Guacamole server environment.
*/
@Inject
private Environment environment;
/**
* Returns the authorization endpoint (URI) of the OpenID service as
* configured with guacamole.properties.
*
* @return
* The authorization endpoint of the OpenID service, as configured with
* guacamole.properties.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or if the authorization
* endpoint property is missing.
*/
public URI getAuthorizationEndpoint() throws GuacamoleException {
return environment.getRequiredProperty(OPENID_AUTHORIZATION_ENDPOINT);
}
/**
* Returns the OpenID client ID which should be submitted to the OpenID
* service when necessary, as configured with guacamole.properties. This
* value is typically provided by the OpenID service when OpenID credentials
* are generated for your application.
*
* @return
* The client ID to use when communicating with the OpenID service,
* as configured with guacamole.properties.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or if the client ID
* property is missing.
*/
public String getClientID() throws GuacamoleException {
return environment.getRequiredProperty(OPENID_CLIENT_ID);
}
/**
* Returns the URI that the OpenID service should redirect to after
* the authentication process is complete, as configured with
* guacamole.properties. This must be the full URL that a user would enter
* into their browser to access Guacamole.
*
* @return
* The client secret to use when communicating with the OpenID service,
* as configured with guacamole.properties.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or if the redirect URI
* property is missing.
*/
public URI getRedirectURI() throws GuacamoleException {
return environment.getRequiredProperty(OPENID_REDIRECT_URI);
}
/**
* Returns the issuer to expect for all received ID tokens, as configured
* with guacamole.properties.
*
* @return
* The issuer to expect for all received ID tokens, as configured with
* guacamole.properties.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or if the issuer property
* is missing.
*/
public String getIssuer() throws GuacamoleException {
return environment.getRequiredProperty(OPENID_ISSUER);
}
/**
* Returns the endpoint (URI) of the JWKS service which defines how
* received ID tokens (JWTs) shall be validated, as configured with
* guacamole.properties.
*
* @return
* The endpoint (URI) of the JWKS service which defines how received ID
* tokens (JWTs) shall be validated, as configured with
* guacamole.properties.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or if the JWKS endpoint
* property is missing.
*/
public URI getJWKSEndpoint() throws GuacamoleException {
return environment.getRequiredProperty(OPENID_JWKS_ENDPOINT);
}
/**
* Returns the claim type which contains the authenticated user's username
* within any valid JWT, as configured with guacamole.properties. By
* default, this will be "email".
*
* @return
* The claim type which contains the authenticated user's username
* within any valid JWT, as configured with guacamole.properties.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getUsernameClaimType() throws GuacamoleException {
return environment.getProperty(OPENID_USERNAME_CLAIM_TYPE, DEFAULT_USERNAME_CLAIM_TYPE);
}
/**
* Returns the claim type which contains the authenticated user's groups
* within any valid JWT, as configured with guacamole.properties. By
* default, this will be "groups".
*
* @return
* The claim type which contains the authenticated user's groups
* within any valid JWT, as configured with guacamole.properties.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getGroupsClaimType() throws GuacamoleException {
return environment.getProperty(OPENID_GROUPS_CLAIM_TYPE, DEFAULT_GROUPS_CLAIM_TYPE);
}
/**
* Returns the space-separated list of OpenID scopes to request. By default,
* this will be "openid email profile". The OpenID scopes determine the
* information returned within the OpenID token, and thus affect what
* values can be used as an authenticated user's username.
*
* @return
* The space-separated list of OpenID scopes to request when identifying
* a user.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getScope() throws GuacamoleException {
return environment.getProperty(OPENID_SCOPE, DEFAULT_SCOPE);
}
/**
* Returns the amount of clock skew tolerated for timestamp comparisons
* between the Guacamole server and OpenID service clocks, in seconds. Too
* much clock skew will affect token expiration calculations, possibly
* allowing old tokens to be used. By default, this will be 30.
*
* @return
* The amount of clock skew tolerated for timestamp comparisons, in
* seconds.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public int getAllowedClockSkew() throws GuacamoleException {
return environment.getProperty(OPENID_ALLOWED_CLOCK_SKEW, DEFAULT_ALLOWED_CLOCK_SKEW);
}
/**
* Returns the maximum amount of time that an OpenID token should remain
* valid, in minutes. A token received from an OpenID service which is
* older than this amount of time will be rejected, even if it is otherwise
* valid. By default, this will be 300 (5 hours).
*
* @return
* The maximum amount of time that an OpenID token should remain valid,
* in minutes.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public int getMaxTokenValidity() throws GuacamoleException {
return environment.getProperty(OPENID_MAX_TOKEN_VALIDITY, DEFAULT_MAX_TOKEN_VALIDITY);
}
/**
* Returns the maximum amount of time that a nonce generated by the
* Guacamole server should remain valid, in minutes. As each OpenID request
* has a unique nonce value, this imposes an upper limit on the amount of
* time any particular OpenID request can result in successful
* authentication within Guacamole. By default, this will be 10.
*
* @return
* The maximum amount of time that a nonce generated by the Guacamole
* server should remain valid, in minutes.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public int getMaxNonceValidity() throws GuacamoleException {
return environment.getProperty(OPENID_MAX_NONCE_VALIDITY, DEFAULT_MAX_NONCE_VALIDITY);
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.openid.form;
import java.net.URI;
import javax.ws.rs.core.UriBuilder;
import org.apache.guacamole.form.RedirectField;
import org.apache.guacamole.language.TranslatableMessage;
/**
* Field definition which represents the token returned by an OpenID Connect
* service.
*/
public class TokenField extends RedirectField {
/**
* The standard HTTP parameter which will be included within the URL by all
* OpenID services upon successful authentication and redirect.
*/
public static final String PARAMETER_NAME = "id_token";
/**
* Creates a new field which requests authentication via OpenID connect.
* Successful authentication at the OpenID Connect service will result in
* the client being redirected to the specified redirect URI. The OpenID
* token will be embedded in the fragment (the part following the hash
* symbol) of that URI, which the JavaScript side of this extension will
* move to the query parameters.
*
* @param authorizationEndpoint
* The full URL of the endpoint accepting OpenID authentication
* requests.
*
* @param scope
* The space-delimited list of OpenID scopes to request from the
* identity provider, such as "openid" or "openid email profile".
*
* @param clientID
* The ID of the OpenID client. This is normally determined ahead of
* time by the OpenID service through some manual credential request
* procedure.
*
* @param redirectURI
* The URI that the OpenID service should redirect to upon successful
* authentication.
*
* @param nonce
* A random string unique to this request. To defend against replay
* attacks, this value must cease being valid after its first use.
*
* @param redirectMessage
* The message that will be displayed to the user during redirect. This
* will be processed through Guacamole's translation system.
*/
public TokenField(URI authorizationEndpoint, String scope,
String clientID, URI redirectURI, String nonce,
TranslatableMessage redirectMessage) {
super(PARAMETER_NAME, UriBuilder.fromUri(authorizationEndpoint)
.queryParam("scope", scope)
.queryParam("response_type", "id_token")
.queryParam("client_id", clientID)
.queryParam("redirect_uri", redirectURI)
.queryParam("nonce", nonce)
.build(),
redirectMessage);
}
}

View File

@@ -0,0 +1,135 @@
/*
* 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.openid.token;
import com.google.inject.Singleton;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Service for generating and validating single-use random tokens (nonces).
*/
@Singleton
public class NonceService {
/**
* Cryptographically-secure random number generator for generating the
* required nonce.
*/
private final SecureRandom random = new SecureRandom();
/**
* Map of all generated nonces to their corresponding expiration timestamps.
* This Map must be periodically swept of expired nonces to avoid growing
* without bound.
*/
private final Map<String, Long> nonces = new ConcurrentHashMap<String, Long>();
/**
* The timestamp of the last expired nonce sweep.
*/
private long lastSweep = System.currentTimeMillis();
/**
* The minimum amount of time to wait between sweeping expired nonces from
* the Map.
*/
private static final long SWEEP_INTERVAL = 60000;
/**
* Iterates through the entire Map of generated nonces, removing any nonce
* that has exceeded its expiration timestamp. If insufficient time has
* elapsed since the last sweep, as dictated by SWEEP_INTERVAL, this
* function has no effect.
*/
private void sweepExpiredNonces() {
// Do not sweep until enough time has elapsed since the last sweep
long currentTime = System.currentTimeMillis();
if (currentTime - lastSweep < SWEEP_INTERVAL)
return;
// Record time of sweep
lastSweep = currentTime;
// For each stored nonce
Iterator<Map.Entry<String, Long>> entries = nonces.entrySet().iterator();
while (entries.hasNext()) {
// Remove all entries which have expired
Map.Entry<String, Long> current = entries.next();
if (current.getValue() <= System.currentTimeMillis())
entries.remove();
}
}
/**
* Generates a cryptographically-secure nonce value. The nonce is intended
* to be used to prevent replay attacks.
*
* @param maxAge
* The maximum amount of time that the generated nonce should remain
* valid, in milliseconds.
*
* @return
* A cryptographically-secure nonce value.
*/
public String generate(long maxAge) {
// Sweep expired nonces if enough time has passed
sweepExpiredNonces();
// Generate and store nonce, along with expiration timestamp
String nonce = new BigInteger(130, random).toString(32);
nonces.put(nonce, System.currentTimeMillis() + maxAge);
return nonce;
}
/**
* Returns whether the give nonce value is valid. A nonce is valid if and
* only if it was generated by this instance of the NonceService. Testing
* nonce validity through this function immediately and permanently
* invalidates that nonce.
*
* @param nonce
* The nonce value to test.
*
* @return
* true if the provided nonce is valid, false otherwise.
*/
public boolean isValid(String nonce) {
// Remove nonce, verifying whether it was present at all
Long expires = nonces.remove(nonce);
if (expires == null)
return false;
// Nonce is only valid if it hasn't expired
return expires > System.currentTimeMillis();
}
}

View File

@@ -0,0 +1,204 @@
/*
* 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.openid.token;
import com.google.inject.Inject;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.guacamole.auth.openid.conf.ConfigurationService;
import org.apache.guacamole.GuacamoleException;
import org.jose4j.jwk.HttpsJwks;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service for validating ID tokens forwarded to us by the client, verifying
* that they did indeed come from the OpenID service.
*/
public class TokenValidationService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(TokenValidationService.class);
/**
* Service for retrieving OpenID configuration information.
*/
@Inject
private ConfigurationService confService;
/**
* Service for validating and generating unique nonce values.
*/
@Inject
private NonceService nonceService;
/**
* Validates the given ID token, returning the JwtClaims contained therein.
* If the ID token is invalid, null is returned.
*
* @param token
* The ID token to validate.
*
* @return
* The JWT claims contained within the given ID token if it passes tests,
* or null if the token is not valid.
*
* @throws GuacamoleException
* If guacamole.properties could not be parsed.
*/
public JwtClaims validateToken(String token) throws GuacamoleException {
// Validating the token requires a JWKS key resolver
HttpsJwks jwks = new HttpsJwks(confService.getJWKSEndpoint().toString());
HttpsJwksVerificationKeyResolver resolver = new HttpsJwksVerificationKeyResolver(jwks);
// Create JWT consumer for validating received token
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
.setRequireExpirationTime()
.setMaxFutureValidityInMinutes(confService.getMaxTokenValidity())
.setAllowedClockSkewInSeconds(confService.getAllowedClockSkew())
.setRequireSubject()
.setExpectedIssuer(confService.getIssuer())
.setExpectedAudience(confService.getClientID())
.setVerificationKeyResolver(resolver)
.build();
try {
// Validate JWT
JwtClaims claims = jwtConsumer.processToClaims(token);
// Verify a nonce is present
String nonce = claims.getStringClaimValue("nonce");
if (nonce != null) {
// Verify that we actually generated the nonce, and that it has not
// already been used
if (nonceService.isValid(nonce)) {
// nonce is valid, consider claims valid
return claims;
}
else {
logger.info("Rejected OpenID token with invalid/old nonce.");
}
}
else {
logger.info("Rejected OpenID token without nonce.");
}
}
// Log any failures to validate/parse the JWT
catch (MalformedClaimException e) {
logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage());
logger.debug("Malformed claim within received JWT.", e);
}
catch (InvalidJwtException e) {
logger.info("Rejected invalid OpenID token: {}", e.getMessage());
logger.debug("Invalid JWT received.", e);
}
return null;
}
/**
* Parses the given JwtClaims, returning the username contained
* therein, as defined by the username claim type given in
* guacamole.properties. If the username claim type is missing or
* is invalid, null is returned.
*
* @param claims
* A valid JwtClaims to extract the username from.
*
* @return
* The username contained within the given JwtClaims, or null if the
* claim is not valid or the username claim type is missing,
*
* @throws GuacamoleException
* If guacamole.properties could not be parsed.
*/
public String processUsername(JwtClaims claims) throws GuacamoleException {
String usernameClaim = confService.getUsernameClaimType();
if (claims != null) {
try {
// Pull username from claims
String username = claims.getStringClaimValue(usernameClaim);
if (username != null)
return username;
}
catch (MalformedClaimException e) {
logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage());
logger.debug("Malformed claim within received JWT.", e);
}
// Warn if username was not present in token, as it likely means
// the system is not set up correctly
logger.warn("Username claim \"{}\" missing from token. Perhaps the "
+ "OpenID scope and/or username claim type are "
+ "misconfigured?", usernameClaim);
}
// Could not retrieve username from JWT
return null;
}
/**
* Parses the given JwtClaims, returning the groups contained
* therein, as defined by the groups claim type given in
* guacamole.properties. If the groups claim type is missing or
* is invalid, an empty set is returned.
*
* @param claims
* A valid JwtClaims to extract groups from.
*
* @return
* A Set of String representing the groups the user is member of
* from the OpenID provider point of view, or an empty Set if
* claim is not valid or the groups claim type is missing,
*
* @throws GuacamoleException
* If guacamole.properties could not be parsed.
*/
public Set<String> processGroups(JwtClaims claims) throws GuacamoleException {
String groupsClaim = confService.getGroupsClaimType();
if (claims != null) {
try {
// Pull groups from claims
List<String> oidcGroups = claims.getStringListClaimValue(groupsClaim);
if (oidcGroups != null && !oidcGroups.isEmpty())
return Collections.unmodifiableSet(new HashSet<>(oidcGroups));
}
catch (MalformedClaimException e) {
logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage());
logger.debug("Malformed claim within received JWT.", e);
}
}
// Could not retrieve groups from JWT
return Collections.emptySet();
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.openid.user;
import com.google.inject.Inject;
import java.util.Set;
import org.apache.guacamole.net.auth.AbstractAuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
/**
* An openid-specific implementation of AuthenticatedUser, associating a
* username, a particular set of credentials and the groups with the
* OpenID authentication provider.
*/
public class AuthenticatedUser extends AbstractAuthenticatedUser {
/**
* Reference to the authentication provider associated with this
* authenticated user.
*/
@Inject
private AuthenticationProvider authProvider;
/**
* The credentials provided when this user was authenticated.
*/
private Credentials credentials;
/**
* The groups of the user that was authenticated.
*/
private Set<String> effectiveGroups;
/**
* Initializes this AuthenticatedUser using the given username and
* credentials.
*
* @param username
* The username of the user that was authenticated.
*
* @param credentials
* The credentials provided when this user was authenticated.
*
* @param effectiveGroups
* The groups of the user that was authenticated.
*/
public void init(String username, Credentials credentials, Set<String> effectiveGroups) {
this.credentials = credentials;
this.effectiveGroups = effectiveGroups;
setIdentifier(username);
}
@Override
public AuthenticationProvider getAuthenticationProvider() {
return authProvider;
}
@Override
public Credentials getCredentials() {
return credentials;
}
@Override
public Set<String> getEffectiveUserGroups() {
return effectiveGroups;
}
}

View File

@@ -0,0 +1,28 @@
{
"guacamoleVersion" : "1.3.0",
"name" : "OpenID Authentication Extension",
"namespace" : "openid",
"authProviders" : [
"org.apache.guacamole.auth.openid.OpenIDAuthenticationProvider"
],
"translations" : [
"translations/ca.json",
"translations/de.json",
"translations/en.json",
"translations/fr.json",
"translations/ja.json",
"translations/ko.json",
"translations/pt.json",
"translations/ru.json",
"translations/zh.json"
],
"js" : [
"openid.min.js"
]
}

View File

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

View File

@@ -0,0 +1,35 @@
/*
* 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.
*/
/**
* Before AngularJS routing takes effect, reformat the URL fragment
* from the format used by OpenID Connect ("#param1=value1&param2=value2&...")
* to the format used by AngularJS ("#/?param1=value1&param2=value2&...") such
* that the client side of Guacamole's authentication system will automatically
* forward the "id_token" value for server-side validation.
*
* Note that not all OpenID identity providers will include the "id_token"
* parameter in the first position; it may occur after several other parameters
* within the fragment.
*/
(function guacOpenIDTransformToken() {
if (/^#(?![?\/])(.*&)?id_token=/.test(location.hash))
location.hash = '/?' + location.hash.substring(1);
})();

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_OPENID" : {
"NAME" : "OpenID SSO Backend"
},
"LOGIN" : {
"FIELD_HEADER_ID_TOKEN" : "",
"INFO_OID_REDIRECT_PENDING" : "Espereu, redirigint al proveïdor d'identitat ..."
}
}

View File

@@ -0,0 +1,7 @@
{
"LOGIN" : {
"INFO_OID_REDIRECT_PENDING" : "Bitte warten, Sie werden zum Identitätsprovider weitergeleitet..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_OPENID" : {
"NAME" : "OpenID SSO Backend"
},
"LOGIN" : {
"FIELD_HEADER_ID_TOKEN" : "",
"INFO_OID_REDIRECT_PENDING" : "Please wait, redirecting to identity provider..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_OPENID" : {
"NAME" : "OpenID SSO Backend"
},
"LOGIN" : {
"FIELD_HEADER_ID_TOKEN" : "",
"INFO_OID_REDIRECT_PENDING" : "Veuillez patienter, redirection vers le fournisseur d'identité..."
}
}

View File

@@ -0,0 +1,7 @@
{
"LOGIN" : {
"INFO_OID_REDIRECT_PENDING" : "IDプロバイダへリダイレクトしています。"
}
}

View File

@@ -0,0 +1,7 @@
{
"LOGIN" : {
"INFO_OID_REDIRECT_PENDING" : "잠시만 기다려주십시오. ID 제공자로 리디렉션 중..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_OPENID" : {
"NAME" : "OpenID SSO Backend"
},
"LOGIN" : {
"FIELD_HEADER_ID_TOKEN" : "",
"INFO_OID_REDIRECT_PENDING" : "Por favor aguarde, redirecionando ao provedor de indentidade..."
}
}

View File

@@ -0,0 +1,11 @@
{
"DATA_SOURCE_OPENID" : {
"NAME" : "Бэкенд OpenID SSO"
},
"LOGIN" : {
"INFO_REDIRECT_PENDING" : "Пожалуйста, подождите. Переадресую на страницу аутентификации..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_OPENID" : {
"NAME" : "OpenID SSO后端"
},
"LOGIN" : {
"FIELD_HEADER_ID_TOKEN" : "",
"INFO_REDIRECT_PENDING" : "请稍候,正在重定向到身份提供者..."
}
}

View File

@@ -0,0 +1,3 @@
*~
target/
src/main/resources/generated/

View File

@@ -0,0 +1 @@
src/main/resources/html/*.html

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-saml</artifactId>
<packaging>jar</packaging>
<version>1.3.0</version>
<name>guacamole-auth-sso-saml</name>
<url>http://guacamole.apache.org/</url>
<parent>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso</artifactId>
<version>1.3.0</version>
<relativePath>../../</relativePath>
</parent>
<dependencies>
<!-- Guacamole Extension API -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
</dependency>
<!-- Core SSO support -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-base</artifactId>
</dependency>
<!-- Guice -->
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
</dependency>
<!-- Java servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</dependency>
<!-- JAX-RS Annotations -->
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
</dependency>
<!-- OneLogin SAML Library -->
<dependency>
<groupId>com.onelogin</groupId>
<artifactId>java-saml</artifactId>
<version>2.6.0</version>
<exclusions>
<!-- Force consistent version of commons-codec (see below) -->
<exclusion>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Force single, specific version of commons-codec (multiple
dependencies reference multiple versions of this). -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
<id>dist</id>
<baseDirectory>${project.artifactId}-${project.version}</baseDirectory>
<!-- Output tar.gz -->
<formats>
<format>tar.gz</format>
</formats>
<!-- Include licenses and extension .jar -->
<fileSets>
<!-- Include licenses -->
<fileSet>
<outputDirectory></outputDirectory>
<directory>target/licenses</directory>
</fileSet>
<!-- Include extension .jar -->
<fileSet>
<directory>target</directory>
<outputDirectory></outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,119 @@
/*
* 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.saml;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.net.URI;
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.auth.saml.user.SAMLAuthenticatedUser;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.saml.acs.AssertedIdentity;
import org.apache.guacamole.auth.saml.acs.AuthenticationSessionManager;
import org.apache.guacamole.auth.saml.acs.SAMLService;
import org.apache.guacamole.form.Field;
import org.apache.guacamole.form.RedirectField;
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;
import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
/**
* Service that authenticates Guacamole users by processing the responses of
* SAML identity providers.
*/
public class AuthenticationProviderService {
/**
* The name of the query parameter that identifies an active authentication
* session (in-progress SAML authentication attempt).
*/
public static final String AUTH_SESSION_QUERY_PARAM = "state";
/**
* Provider for AuthenticatedUser objects.
*/
@Inject
private Provider<SAMLAuthenticatedUser> authenticatedUserProvider;
/**
* Manager of active SAML authentication attempts.
*/
@Inject
private AuthenticationSessionManager sessionManager;
/**
* Service for processing SAML requests/responses.
*/
@Inject
private SAMLService saml;
/**
* Returns an AuthenticatedUser representing the user authenticated by the
* given credentials.
*
* @param credentials
* The credentials to use for authentication.
*
* @return
* An AuthenticatedUser representing the user authenticated by the
* given credentials.
*
* @throws GuacamoleException
* If an error occurs while authenticating the user, or if access is
* denied.
*/
public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
// No authentication can be attempted without a corresponding HTTP
// request
HttpServletRequest request = credentials.getRequest();
if (request == null)
return null;
// Use established SAML identity if already provided by the SAML IdP
AssertedIdentity identity = sessionManager.getIdentity(request.getParameter(AUTH_SESSION_QUERY_PARAM));
if (identity != null) {
// Back-port the username to the credentials
credentials.setUsername(identity.getUsername());
// Configure the AuthenticatedUser and return it
SAMLAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
authenticatedUser.init(identity, credentials);
return authenticatedUser;
}
// Redirect to SAML IdP if no SAML identity is associated with the
// Guacamole authentication request
URI authUri = saml.createRequest();
throw new GuacamoleInsufficientCredentialsException("Redirecting to SAML IdP.",
new CredentialsInfo(Arrays.asList(new Field[] {
new RedirectField("samlRedirect", authUri, new TranslatableMessage("LOGIN.INFO_SAML_REDIRECT_PENDING"))
}))
);
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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.saml;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.saml.acs.AssertionConsumerServiceResource;
import org.apache.guacamole.auth.saml.acs.AuthenticationSessionManager;
import org.apache.guacamole.auth.saml.user.SAMLAuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.TokenInjectingUserContext;
import org.apache.guacamole.net.auth.UserContext;
/**
* AuthenticationProvider implementation that authenticates Guacamole users
* against a SAML SSO Identity Provider (IdP). This module does not provide any
* storage for connection information, and must be layered with other modules
* for authenticated users to have access to Guacamole connections.
*/
public class SAMLAuthenticationProvider extends AbstractAuthenticationProvider {
/**
* Injector which will manage the object graph of this authentication
* provider.
*/
private final Injector injector;
/**
* Creates a new SAMLAuthenticationProvider that authenticates users
* against a SAML IdP.
*/
public SAMLAuthenticationProvider() {
// Set up Guice injector.
injector = Guice.createInjector(
new SAMLAuthenticationProviderModule(this)
);
}
@Override
public String getIdentifier() {
return "saml";
}
@Override
public Object getResource() throws GuacamoleException {
return injector.getInstance(AssertionConsumerServiceResource.class);
}
@Override
public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
// Attempt to authenticate user with given credentials
AuthenticationProviderService authProviderService =
injector.getInstance(AuthenticationProviderService.class);
return authProviderService.authenticateUser(credentials);
}
@Override
public UserContext decorate(UserContext context,
AuthenticatedUser authenticatedUser, Credentials credentials)
throws GuacamoleException {
// Only decorate if the user authenticated with SAML
if (!(authenticatedUser instanceof SAMLAuthenticatedUser))
return context;
// Apply SAML-specific tokens to all connections / connection groups
return new TokenInjectingUserContext(context,
((SAMLAuthenticatedUser) authenticatedUser).getTokens());
}
@Override
public void shutdown() {
injector.getInstance(AuthenticationSessionManager.class).shutdown();
}
}

View File

@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.saml;
import com.google.inject.AbstractModule;
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
import org.apache.guacamole.auth.saml.acs.AssertionConsumerServiceResource;
import org.apache.guacamole.auth.saml.acs.AuthenticationSessionManager;
import org.apache.guacamole.auth.saml.acs.IdentifierGenerator;
import org.apache.guacamole.auth.saml.acs.SAMLService;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.net.auth.AuthenticationProvider;
/**
* Guice module which configures SAML-specific injections.
*/
public class SAMLAuthenticationProviderModule extends AbstractModule {
/**
* Guacamole server environment.
*/
private final Environment environment;
/**
* A reference to the SAMLAuthenticationProvider on behalf of which this
* module has configured injection.
*/
private final AuthenticationProvider authProvider;
/**
* Creates a new SAML authentication provider module which configures
* injection for the SAMLAuthenticationProvider.
*
* @param authProvider
* The AuthenticationProvider for which injection is being configured.
*/
public SAMLAuthenticationProviderModule(AuthenticationProvider authProvider) {
// Get local environment
this.environment = LocalEnvironment.getInstance();
// Store associated auth provider
this.authProvider = authProvider;
}
@Override
protected void configure() {
// Bind core implementations of guacamole-ext classes
bind(AuthenticationProvider.class).toInstance(authProvider);
bind(Environment.class).toInstance(environment);
// Bind SAML-specific services
bind(AssertionConsumerServiceResource.class);
bind(AuthenticationSessionManager.class);
bind(ConfigurationService.class);
bind(IdentifierGenerator.class);
bind(SAMLService.class);
}
}

View File

@@ -0,0 +1,134 @@
/*
* 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.saml.acs;
import com.onelogin.saml2.authn.SamlResponse;
import com.onelogin.saml2.exception.ValidationError;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.xml.xpath.XPathExpressionException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException;
/**
* Representation of a user's identity as asserted by a SAML IdP.
*/
public class AssertedIdentity {
/**
* The original SAML response received from the SAML IdP that asserted
* the user's identity.
*/
private final SamlResponse response;
/**
* The user's Guacamole username.
*/
private final String username;
/**
* All attributes included in the original SAML response. Attributes may
* possibly be associated with multiple values.
*/
private final Map<String, List<String>> attributes;
/**
* Creates a new AssertedIdentity representing the identity asserted by the
* SAML IdP in the given response.
*
* @param response
* The response received from the SAML IdP.
*
* @throws GuacamoleException
* If the given SAML response cannot be parsed or is missing required
* information.
*/
public AssertedIdentity(SamlResponse response) throws GuacamoleException {
// Parse user identity from SAML response
String nameId;
try {
nameId = response.getNameId();
if (nameId == null)
throw new GuacamoleSecurityException("SAML response did not "
+ "include the relevant user's identity (no name ID).");
}
// Unfortunately, getNameId() is declared as "throws Exception", so
// this error handling has to be pretty generic
catch (Exception e) {
throw new GuacamoleSecurityException("User identity (name ID) "
+ "could not be retrieved from the SAML response: " + e.getMessage(), e);
}
// Retrieve any provided attributes
Map<String, List<String>> responseAttributes;
try {
responseAttributes = Collections.unmodifiableMap(response.getAttributes());
}
catch (XPathExpressionException | ValidationError e) {
throw new GuacamoleSecurityException("SAML attributes could not "
+ "be parsed from the SAML response: " + e.getMessage(), e);
}
this.response = response;
this.username = nameId.toLowerCase(); // Canonicalize username as lowercase
this.attributes = responseAttributes;
}
/**
* Returns the username of the Guacamole user whose identity was asserted
* by the SAML IdP.
*
* @return
* The username of the Guacamole user whose identity was asserted by
* the SAML IdP.
*/
public String getUsername() {
return username;
}
/**
* Returns a Map containing all attributes included in the original SAML
* response that asserted this user's identity. Attributes may possibly be
* associated with multiple values.
*
* @return
* A Map of all attributes included in the original SAML response.
*/
public Map<String, List<String>> getAttributes() {
return attributes;
}
/**
* Returns whether the identity asserted by the original SAML response is
* still valid. An asserted identity may cease to be valid after creation
* if it has expired according to the timestamps included in the response.
*
* @return
* true if the original SAML response is still valid, false otherwise.
*/
public boolean isValid() {
return response.isValid();
}
}

View File

@@ -0,0 +1,134 @@
/*
* 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.saml.acs;
import com.google.inject.Inject;
import java.net.URI;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Response;
import javax.ws.rs.FormParam;
import javax.ws.rs.Path;
import javax.ws.rs.POST;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.UriBuilder;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.saml.AuthenticationProviderService;
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* REST API resource that provides a SAML assertion consumer service (ACS)
* endpoint. SAML identity providers will issue an HTTP POST to this endpoint
* asserting the user's identity when the user has successfully authenticated.
*/
public class AssertionConsumerServiceResource {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(AssertionConsumerServiceResource.class);
/**
* The configuration service for this module.
*/
@Inject
private ConfigurationService confService;
/**
* Manager of active SAML authentication attempts.
*/
@Inject
private AuthenticationSessionManager sessionManager;
/**
* Service for processing SAML requests/responses.
*/
@Inject
private SAMLService saml;
/**
* Processes the SAML response submitted by the SAML IdP via an HTTP POST.
* If SSO has been successful, the user is redirected back to Guacamole to
* complete authentication. If SSO has failed, the user is redirected back
* to Guacamole to re-attempt authentication.
*
* @param relayState
* The "RelayState" value originally provided in the SAML request,
* which in our case is the transient the session identifier of the
* in-progress authentication attempt. The SAML standard requires that
* the identity provider include the "RelayState" value it received
* alongside its SAML response.
*
* @param samlResponse
* The encoded response returned by the SAML IdP.
*
* @param consumedRequest
* The HttpServletRequest associated with the SAML response. The
* parameters of this request may not be accessible, as the request may
* have been fully consumed by JAX-RS.
*
* @return
* An HTTP Response that will redirect the user back to Guacamole,
* with an internal state identifier included in the URL such that the
* the Guacamole side of authentication can complete.
*
* @throws GuacamoleException
* If configuration information required for processing SAML responses
* cannot be read.
*/
@POST
@Path("callback")
public Response processSamlResponse(
@FormParam("RelayState") String relayState,
@FormParam("SAMLResponse") String samlResponse,
@Context HttpServletRequest consumedRequest)
throws GuacamoleException {
URI guacBase = confService.getCallbackUrl();
try {
// Validate and parse identity asserted by SAML IdP
AuthenticationSession session = saml.processResponse(
consumedRequest.getRequestURL().toString(),
relayState, samlResponse);
// Store asserted identity for future retrieval via redirect
String identifier = sessionManager.defer(session);
// Redirect user such that Guacamole's authentication system can
// retrieve the relevant SAML identity (stored above)
return Response.seeOther(UriBuilder.fromUri(guacBase)
.queryParam(AuthenticationProviderService.AUTH_SESSION_QUERY_PARAM, identifier)
.build()
).build();
}
// If invalid, redirect back to main page to re-attempt authentication
catch (GuacamoleException e) {
logger.warn("Authentication attempted with an invalid SAML response: {}", e.getMessage());
logger.debug("Received SAML response failed validation.", e);
return Response.seeOther(guacBase).build();
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* 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.saml.acs;
/**
* Representation of an in-progress SAML authentication attempt.
*/
public class AuthenticationSession {
/**
* The absolute point in time after which this authentication session is
* invalid. This value is a UNIX epoch timestamp, as may be returned by
* {@link System#currentTimeMillis()}.
*/
private final long expirationTimestamp;
/**
* The request ID of the SAML request associated with the authentication
* attempt.
*/
private final String requestId;
/**
* The identity asserted by the SAML IdP, or null if authentication has not
* yet completed successfully.
*/
private AssertedIdentity identity = null;
/**
* Creates a new AuthenticationSession representing an in-progress SAML
* authentication attempt.
*
* @param requestId
* The request ID of the SAML request associated with the
* authentication attempt.
*
* @param expires
* The number of milliseconds that may elapse before this session must
* be considered invalid.
*/
public AuthenticationSession(String requestId, long expires) {
this.expirationTimestamp = System.currentTimeMillis() + expires;
this.requestId = requestId;
}
/**
* Returns whether this authentication session is still valid (has not yet
* expired). If an identity has been asserted by the SAML IdP, this
* considers also whether the SAML response asserting that identity has
* expired.
*
* @return
* true if this authentication session is still valid, false if it has
* expired.
*/
public boolean isValid() {
return System.currentTimeMillis() < expirationTimestamp
&& (identity == null || identity.isValid());
}
/**
* Returns the request ID of the SAML request associated with the
* authentication attempt.
*
* @return
* The request ID of the SAML request associated with the
* authentication attempt.
*/
public String getRequestID() {
return requestId;
}
/**
* Marks this authentication attempt as completed and successful, with the
* user having been asserted as having the given identity by the SAML IdP.
*
* @param identity
* The identity asserted by the SAML IdP.
*/
public void setIdentity(AssertedIdentity identity) {
this.identity = identity;
}
/**
* Returns the identity asserted by the SAML IdP. If authentication has not
* yet completed successfully, this will be null.
*
* @return
* The identity asserted by the SAML IdP, or null if authentication has
* not yet completed successfully.
*/
public AssertedIdentity getIdentity() {
return identity;
}
}

View File

@@ -0,0 +1,152 @@
/*
* 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.saml.acs;
import com.google.common.base.Predicates;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Manager service that temporarily stores SAML authentication attempts while
* the authentication flow is underway. Authentication attempts are represented
* as temporary authentication sessions, allowing authentication attempts to
* span multiple requests and redirects. Invalid or stale authentication
* sessions are automatically purged from storage.
*/
@Singleton
public class AuthenticationSessionManager {
/**
* Generator of arbitrary, unique, unpredictable identifiers.
*/
@Inject
private IdentifierGenerator idGenerator;
/**
* Map of authentication session identifiers to their associated
* {@link AuthenticationSession}.
*/
private final ConcurrentMap<String, AuthenticationSession> sessions =
new ConcurrentHashMap<>();
/**
* Executor service which runs the periodic cleanup task
*/
private final ScheduledExecutorService executor =
Executors.newScheduledThreadPool(1);
/**
* Creates a new AuthenticationSessionManager that manages in-progress
* SAML authentication attempts. Invalid, stale sessions are automatically
* cleaned up.
*/
public AuthenticationSessionManager() {
executor.scheduleAtFixedRate(() -> {
sessions.values().removeIf(Predicates.not(AuthenticationSession::isValid));
}, 1, 1, TimeUnit.MINUTES);
}
/**
* Resumes the Guacamole side of the authentication process that was
* previously deferred through a call to defer(). Once invoked, the
* provided value ceases to be valid for future calls to resume().
*
* @param identifier
* The unique string returned by the call to defer(). For convenience,
* this value may safely be null.
*
* @return
* The {@link AuthenticationSession} originally provided when defer()
* was invoked, or null if the session is no longer valid or no such
* value was returned by defer().
*/
public AuthenticationSession resume(String identifier) {
if (identifier != null) {
AuthenticationSession session = sessions.remove(identifier);
if (session != null && session.isValid())
return session;
}
return null;
}
/**
* Returns the identity finally asserted by the SAML IdP at the end of the
* authentication process represented by the authentication session with
* the given identifier. If there is no such authentication session, or no
* valid identity has been asserted by the SAML IdP for that session, null
* is returned.
*
* @param identifier
* The unique string returned by the call to defer(). For convenience,
* this value may safely be null.
*
* @return
* The identity finally asserted by the SAML IdP at the end of the
* authentication process represented by the authentication session
* with the given identifier, or null if there is no such identity.
*/
public AssertedIdentity getIdentity(String identifier) {
AuthenticationSession session = resume(identifier);
if (session != null)
return session.getIdentity();
return null;
}
/**
* Defers the Guacamole side of authentication for the user having the
* given authentication session such that it may be later resumed through a
* call to resume(). If authentication is never resumed, the session will
* automatically be cleaned up after it ceases to be valid.
*
* @param session
* The {@link AuthenticationSession} representing the in-progress SAML
* authentication attempt.
*
* @return
* A unique and unpredictable string that may be used to represent the
* given session when calling resume().
*/
public String defer(AuthenticationSession session) {
String identifier = idGenerator.generateIdentifier();
sessions.put(identifier, session);
return identifier;
}
/**
* Shuts down the executor service that periodically removes all invalid
* authentication sessions. This must be invoked when the SAML extension is
* shut down in order to avoid resource leaks.
*/
public void shutdown() {
executor.shutdownNow();
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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.saml.acs;
import com.google.common.io.BaseEncoding;
import com.google.inject.Singleton;
import java.security.SecureRandom;
/**
* Generator of unique and unpredictable identifiers. Each generated identifier
* is an arbitrary, random string produced using a cryptographically-secure
* random number generator and consists of at least 256 bits.
*/
@Singleton
public class IdentifierGenerator {
/**
* Cryptographically-secure random number generator for generating unique
* identifiers.
*/
private final SecureRandom secureRandom = new SecureRandom();
/**
* Generates a unique and unpredictable identifier. Each identifier is at
* least 256-bit and produced using a cryptographically-secure random
* number generator.
*
* @return
* A unique and unpredictable identifier.
*/
public String generateIdentifier() {
byte[] bytes = new byte[33];
secureRandom.nextBytes(bytes);
return BaseEncoding.base64().encode(bytes);
}
}

View File

@@ -0,0 +1,188 @@
/*
* 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.saml.acs;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.onelogin.saml2.authn.AuthnRequest;
import com.onelogin.saml2.authn.SamlResponse;
import com.onelogin.saml2.exception.SettingsException;
import com.onelogin.saml2.exception.ValidationError;
import com.onelogin.saml2.settings.Saml2Settings;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import javax.ws.rs.core.UriBuilder;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
import org.xml.sax.SAXException;
/**
* Service which abstracts the internals of handling SAML requests and
* responses.
*/
@Singleton
public class SAMLService {
/**
* Service for retrieving SAML configuration information.
*/
@Inject
private ConfigurationService confService;
/**
* Manager of active SAML authentication attempts.
*/
@Inject
private AuthenticationSessionManager sessionManager;
/**
* Creates a new SAML request, beginning the overall authentication flow
* that will ultimately result in an asserted user identity if the user is
* successfully authenticated by the SAML IdP. The URI of the SSO endpoint
* of the SAML IdP that the user must be redirected to for the
* authentication process to continue is returned.
*
* @return
* The URI of the SSO endpoint of the SAML IdP that the user must be
* redirected to.
*
* @throws GuacamoleException
* If an error prevents the SAML request and redirect URI from being
* generated.
*/
public URI createRequest() throws GuacamoleException {
Saml2Settings samlSettings = confService.getSamlSettings();
AuthnRequest samlReq = new AuthnRequest(samlSettings);
// Create a new authentication session to represent this attempt while
// it is in progress
AuthenticationSession session = new AuthenticationSession(samlReq.getId(),
confService.getAuthenticationTimeout() * 60000L);
// Produce redirect for continuing the authentication process with
// the SAML IdP
try {
return UriBuilder.fromUri(samlSettings.getIdpSingleSignOnServiceUrl().toURI())
.queryParam("SAMLRequest", samlReq.getEncodedAuthnRequest())
.queryParam("RelayState", sessionManager.defer(session))
.build();
}
catch (IOException e) {
throw new GuacamoleServerException("SAML authentication request "
+ "could not be encoded: " + e.getMessage());
}
catch (URISyntaxException e) {
throw new GuacamoleServerException("SAML IdP redirect could not "
+ "be generated due to an error in the URI syntax: "
+ e.getMessage());
}
}
/**
* Processes the given SAML response, as received by the SAML ACS endpoint
* at the given URL, producing an {@link AuthenticationSession} that now
* includes a valid assertion of the user's identity. If the SAML response
* is invalid in any way, an exception is thrown.
*
* @param url
* The URL of the ACS endpoint that received the SAML response. This
* should be the URL pointing to the single POST-handling endpoint of
* {@link AssertionConsumerServiceResource}.
*
* @param relayState
* The "RelayState" value originally provided in the SAML request,
* which in our case is the transient the session identifier of the
* in-progress authentication attempt. The SAML standard requires that
* the identity provider include the "RelayState" value it received
* alongside its SAML response.
*
* @param encodedResponse
* The response received from the SAML IdP via the ACS endpoint at the
* given URL.
*
* @return
* The {@link AuthenticationSession} associated with the in-progress
* authentication attempt, now associated with the {@link AssertedIdentity}
* representing the identity of the user asserted by the SAML IdP.
*
* @throws GuacamoleException
* If the given SAML response is not valid, or if the configuration
* information required to validate or decrypt the response cannot be
* read.
*/
public AuthenticationSession processResponse(String url, String relayState,
String encodedResponse) throws GuacamoleException {
if (relayState == null)
throw new GuacamoleSecurityException("\"RelayState\" value "
+ "is missing from SAML response.");
AuthenticationSession session = sessionManager.resume(relayState);
if (session == null)
throw new GuacamoleSecurityException("\"RelayState\" value "
+ "included with SAML response is not valid.");
try {
// Decode received SAML response
SamlResponse response = new SamlResponse(confService.getSamlSettings(),
url, encodedResponse);
// Validate SAML response timestamp, signature, etc.
if (!response.isValid(session.getRequestID())) {
Exception validationException = response.getValidationException();
throw new GuacamoleSecurityException("SAML response did not "
+ "pass validation: " + validationException.getMessage(),
validationException);
}
// Parse identity asserted by SAML IdP
session.setIdentity(new AssertedIdentity(response));
return session;
}
catch (ValidationError e) {
throw new GuacamoleSecurityException("SAML response did not pass "
+ "validation: " + e.getMessage(), e);
}
catch (SettingsException e) {
throw new GuacamoleServerException("Current SAML settings are "
+ "insufficient to decrypt/parse the received SAML "
+ "response.", e);
}
catch (ParserConfigurationException | SAXException | XPathExpressionException e) {
throw new GuacamoleServerException("XML contents of SAML "
+ "response could not be parsed.", e);
}
catch (IOException e) {
throw new GuacamoleServerException("Contents of SAML response "
+ "could not be decrypted/read.", e);
}
}
}

View File

@@ -0,0 +1,394 @@
/*
* 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.saml.conf;
import com.google.inject.Inject;
import com.onelogin.saml2.settings.IdPMetadataParser;
import com.onelogin.saml2.settings.Saml2Settings;
import com.onelogin.saml2.settings.SettingsBuilder;
import com.onelogin.saml2.util.Constants;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.core.UriBuilder;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.properties.BooleanGuacamoleProperty;
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 SAML
* authentication module.
*/
public class ConfigurationService {
/**
* The URI of the file containing the XML Metadata associated with the
* SAML IdP.
*/
private static final URIGuacamoleProperty SAML_IDP_METADATA_URL =
new URIGuacamoleProperty() {
@Override
public String getName() { return "saml-idp-metadata-url"; }
};
/**
* The URL of the SAML IdP.
*/
private static final URIGuacamoleProperty SAML_IDP_URL =
new URIGuacamoleProperty() {
@Override
public String getName() { return "saml-idp-url"; }
};
/**
* The URL identifier for this SAML client.
*/
private static final URIGuacamoleProperty SAML_ENTITY_ID =
new URIGuacamoleProperty() {
@Override
public String getName() { return "saml-entity-id"; }
};
/**
* The callback URL to use for SAML IdP, normally the base
* of the Guacamole install. The SAML extensions callback
* endpoint will be appended to this value.
*/
private static final URIGuacamoleProperty SAML_CALLBACK_URL =
new URIGuacamoleProperty() {
@Override
public String getName() { return "saml-callback-url"; }
};
/**
* Whether or not debugging should be enabled in the SAML library to help
* track down errors.
*/
private static final BooleanGuacamoleProperty SAML_DEBUG =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "saml-debug"; }
};
/**
* Whether or not to enable compression for the SAML request.
*/
private static final BooleanGuacamoleProperty SAML_COMPRESS_REQUEST =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "saml-compress-request"; }
};
/**
* Whether or not to enable compression for the SAML response.
*/
private static final BooleanGuacamoleProperty SAML_COMPRESS_RESPONSE =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "saml-compress-response"; }
};
/**
* Whether or not to enforce strict SAML security during processing.
*/
private static final BooleanGuacamoleProperty SAML_STRICT =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "saml-strict"; }
};
/**
* The property that defines what attribute the SAML provider will return
* that contains group membership for the authenticated user.
*/
private static final StringGuacamoleProperty SAML_GROUP_ATTRIBUTE =
new StringGuacamoleProperty() {
@Override
public String getName() { return "saml-group-attribute"; }
};
/**
* The maximum amount of time to allow for an in-progress SAML
* authentication attempt to be completed, in minutes. A user that takes
* longer than this amount of time to complete authentication with their
* identity provider will be redirected back to the identity provider to
* try again.
*/
private static final IntegerGuacamoleProperty SAML_AUTH_TIMEOUT =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "saml-auth-timeout"; }
};
/**
* The Guacamole server environment.
*/
@Inject
private Environment environment;
/**
* Returns the URL to be submitted as the client ID to the SAML IdP, as
* configured in guacamole.properties.
*
* @return
* The URL to send to the SAML IdP as the Client Identifier.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private URI getEntityId() throws GuacamoleException {
return environment.getProperty(SAML_ENTITY_ID);
}
/**
* The URI that contains the metadata that the SAML client should
* use to communicate with the SAML IdP. This can either be a remote
* URL of a server that provides this, or can be a URI to a file on the
* local filesystem. The metadata file is usually generated by the SAML IdP
* and should be uploaded to the system where the Guacamole client is
* running.
*
* @return
* The URI of the file containing the metadata used by the SAML client
* when it communicates with the SAML IdP.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or if the client
* metadata is missing.
*/
private URI getIdpMetadata() throws GuacamoleException {
return environment.getProperty(SAML_IDP_METADATA_URL);
}
/**
* Return the URL used to log in to the SAML IdP.
*
* @return
* The URL used to log in to the SAML IdP.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private URI getIdpUrl() throws GuacamoleException {
return environment.getProperty(SAML_IDP_URL);
}
/**
* The callback URL used for the SAML IdP to POST a response
* to upon completion of authentication, normally the base
* of the Guacamole install.
*
* @return
* The callback URL to be sent to the SAML IdP that will
* be POSTed to upon completion of SAML authentication.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or the property
* is missing.
*/
public URI getCallbackUrl() throws GuacamoleException {
return environment.getRequiredProperty(SAML_CALLBACK_URL);
}
/**
* Return the Boolean value that indicates whether SAML client debugging
* will be enabled, as configured in guacamole.properties. The default is
* false, and debug information will not be generated or logged.
*
* @return
* True if debugging should be enabled in the SAML library, otherwise
* false.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private boolean getDebug() throws GuacamoleException {
return environment.getProperty(SAML_DEBUG, false);
}
/**
* Return the Boolean value that indicates whether or not compression of
* SAML requests to the IdP should be enabled or not, as configured in
* guacamole.properties. The default is to enable compression.
*
* @return
* True if compression should be enabled when sending the SAML request,
* otherwise false.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private boolean getCompressRequest() throws GuacamoleException {
return environment.getProperty(SAML_COMPRESS_REQUEST, true);
}
/**
* Return a Boolean value that indicates whether or not the SAML login
* should enforce strict security controls, as configured in
* guacamole.properties. By default this is true, and should be set to
* true in any production environment.
*
* @return
* True if the SAML login should enforce strict security checks,
* otherwise false.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private boolean getStrict() throws GuacamoleException {
return environment.getProperty(SAML_STRICT, true);
}
/**
* Return a Boolean value that indicates whether or not compression should
* be requested from the server when the SAML response is returned, as
* configured in guacamole.properties. The default is to request that the
* response be compressed.
*
* @return
* True if compression should be requested from the server for the SAML
* response.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private boolean getCompressResponse() throws GuacamoleException {
return environment.getProperty(SAML_COMPRESS_RESPONSE, true);
}
/**
* Return the name of the attribute that will be supplied by the identity
* provider that contains the groups of which this user is a member.
*
* @return
* The name of the attribute that contains the user groups.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getGroupAttribute() throws GuacamoleException {
return environment.getProperty(SAML_GROUP_ATTRIBUTE, "groups");
}
/**
* Returns the maximum amount of time to allow for an in-progress SAML
* authentication attempt to be completed, in minutes. A user that takes
* longer than this amount of time to complete authentication with their
* identity provider will be redirected back to the identity provider to
* try again.
*
* @return
* The maximum amount of time to allow for an in-progress SAML
* authentication attempt to be completed, in minutes.
*
* @throws GuacamoleException
* If the authentication timeout cannot be parsed.
*/
public int getAuthenticationTimeout() throws GuacamoleException {
return environment.getProperty(SAML_AUTH_TIMEOUT, 5);
}
/**
* Returns the collection of SAML settings used to initialize the client.
*
* @return
* The collection of SAML settings used to initialize the SAML client.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed or if required parameters
* are missing.
*/
public Saml2Settings getSamlSettings() throws GuacamoleException {
// Try to get the XML file, first.
URI idpMetadata = getIdpMetadata();
Map<String, Object> samlMap;
if (idpMetadata != null) {
try {
samlMap = IdPMetadataParser.parseRemoteXML(idpMetadata.toURL());
}
catch (Exception e) {
throw new GuacamoleServerException(
"Could not parse SAML IdP Metadata file.", e);
}
}
// If no XML metadata is provided, fall-back to individual values.
else {
samlMap = new HashMap<>();
samlMap.put(SettingsBuilder.IDP_ENTITYID_PROPERTY_KEY,
getIdpUrl().toString());
samlMap.put(SettingsBuilder.IDP_SINGLE_SIGN_ON_SERVICE_URL_PROPERTY_KEY,
getIdpUrl().toString());
samlMap.put(SettingsBuilder.IDP_SINGLE_SIGN_ON_SERVICE_BINDING_PROPERTY_KEY,
Constants.BINDING_HTTP_REDIRECT);
}
// Read entity ID from properties if not provided within metadata XML
if (!samlMap.containsKey(SettingsBuilder.SP_ENTITYID_PROPERTY_KEY)) {
URI entityId = getEntityId();
if (entityId == null)
throw new GuacamoleServerException("SAML Entity ID was not found"
+ " in either the metadata XML file or guacamole.properties");
samlMap.put(SettingsBuilder.SP_ENTITYID_PROPERTY_KEY, entityId.toString());
}
// Derive ACS URL from properties if not provided within metadata XML
if (!samlMap.containsKey(SettingsBuilder.SP_ASSERTION_CONSUMER_SERVICE_URL_PROPERTY_KEY)) {
samlMap.put(SettingsBuilder.SP_ASSERTION_CONSUMER_SERVICE_URL_PROPERTY_KEY,
UriBuilder.fromUri(getCallbackUrl()).path("api/ext/saml/callback").build().toString());
}
SettingsBuilder samlBuilder = new SettingsBuilder();
Saml2Settings samlSettings = samlBuilder.fromValues(samlMap).build();
samlSettings.setStrict(getStrict());
samlSettings.setDebug(getDebug());
samlSettings.setCompressRequest(getCompressRequest());
samlSettings.setCompressResponse(getCompressResponse());
return samlSettings;
}
}

View File

@@ -0,0 +1,178 @@
/*
* 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.saml.user;
import com.google.inject.Inject;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.saml.acs.AssertedIdentity;
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
import org.apache.guacamole.net.auth.AbstractAuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.token.TokenName;
/**
* A SAML-specific implementation of AuthenticatedUser, associating a SAML
* identity and particular set of credentials with the SAML authentication
* provider.
*/
public class SAMLAuthenticatedUser extends AbstractAuthenticatedUser {
/**
* The prefix that should be prepended to all parameter tokens generated
* from SAML attributes.
*/
private static final String SAML_ATTRIBUTE_TOKEN_PREFIX = "SAML_";
/**
* Service for retrieving SAML configuration information.
*/
@Inject
private ConfigurationService confService;
/**
* Reference to the authentication provider associated with this
* authenticated user.
*/
@Inject
private AuthenticationProvider authProvider;
/**
* The credentials provided when this user was authenticated.
*/
private Credentials credentials;
/**
* The effective groups of the authenticated user.
*/
private Set<String> effectiveGroups;
/**
* Tokens associated with the authenticated user.
*/
private Map<String, String> tokens;
/**
* Returns a Map of all parameter tokens that should be made available for
* substitution based on the given {@link AssertedIdentity}. The resulting
* Map will contain one parameter token for each SAML attribute in the
* SAML response that originally asserted the user's identity. Attributes
* that have multiple values will be reduced to a single value, taking the
* first available value and discarding the remaining values.
*
* @param identity
* The {@link AssertedIdentity} representing the user identity
* asserted by the SAML IdP.
*
* @return
* A Map of key and single value pairs that should be made available
* for substitution as parameter tokens.
*/
private Map<String, String> getTokens(AssertedIdentity identity) {
return Collections.unmodifiableMap(identity.getAttributes().entrySet()
.stream()
.filter((entry) -> !entry.getValue().isEmpty())
.collect(Collectors.toMap(
(entry) -> TokenName.canonicalize(entry.getKey(), SAML_ATTRIBUTE_TOKEN_PREFIX),
(entry) -> entry.getValue().get(0)
)));
}
/**
* Returns a set of all group memberships asserted by the SAML IdP.
*
* @param identity
* The {@link AssertedIdentity} representing the user identity
* asserted by the SAML IdP.
*
* @return
* A set of all groups that the SAML IdP asserts this user is a
* member of.
*
* @throws GuacamoleException
* If the configuration information necessary to retrieve group
* memberships from a SAML response cannot be read.
*/
private Set<String> getGroups(AssertedIdentity identity)
throws GuacamoleException {
List<String> samlGroups = identity.getAttributes().get(confService.getGroupAttribute());
if (samlGroups == null || samlGroups.isEmpty())
return Collections.emptySet();
return Collections.unmodifiableSet(new HashSet<>(samlGroups));
}
/**
* Initializes this AuthenticatedUser using the given
* {@link AssertedIdentity} and credentials.
*
* @param identity
* The {@link AssertedIdentity} representing the user identity
* asserted by the SAML IdP.
*
* @param credentials
* The credentials provided when this user was authenticated.
*
* @throws GuacamoleException
* If configuration information required for processing the user's
* identity and group memberships cannot be read.
*/
public void init(AssertedIdentity identity, Credentials credentials)
throws GuacamoleException {
this.credentials = credentials;
this.effectiveGroups = getGroups(identity);
this.tokens = getTokens(identity);
setIdentifier(identity.getUsername());
}
/**
* Returns a Map of tokens associated with this authenticated user.
*
* @return
* A map of token names and values available from this user account.
*/
public Map<String, String> getTokens() {
return tokens;
}
@Override
public AuthenticationProvider getAuthenticationProvider() {
return authProvider;
}
@Override
public Credentials getCredentials() {
return credentials;
}
@Override
public Set<String> getEffectiveUserGroups() {
return effectiveGroups;
}
}

View File

@@ -0,0 +1,20 @@
{
"guacamoleVersion" : "1.3.0",
"name" : "SAML Authentication Extension",
"namespace" : "saml",
"authProviders" : [
"org.apache.guacamole.auth.saml.SAMLAuthenticationProvider"
],
"translations" : [
"translations/ca.json",
"translations/en.json",
"translations/ko.json",
"translations/fr.json",
"translations/pt.json"
]
}

View File

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

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_SAML" : {
"NAME" : "Extensión de autenticación SAML"
},
"LOGIN" : {
"FIELD_HEADER_SAML" : "",
"INFO_SAML_REDIRECT_PENDING" : "Por favor espere, redirigiendo al proveedor de identidad ..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_SAML" : {
"NAME" : "SAML Authentication Extension"
},
"LOGIN" : {
"FIELD_HEADER_SAML" : "",
"INFO_SAML_REDIRECT_PENDING" : "Please wait, redirecting to identity provider..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_SAML" : {
"NAME" : "SAML Authentication Extension"
},
"LOGIN" : {
"FIELD_HEADER_SAML" : "",
"INFO_SAML_REDIRECT_PENDING" : "Veuillez patienter, redirection vers le fournisseur d'identité..."
}
}

View File

@@ -0,0 +1,11 @@
{
"DATA_SOURCE_SAML" : {
"NAME" : "SAML 인증 확장 프로그램"
},
"LOGIN" : {
"INFO_SAML_REDIRECT_PENDING": "잠시만 기다려주십시오. ID 제공자로 리디렉션 중..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_SAML" : {
"NAME" : "SAML Authentication Extension"
},
"LOGIN" : {
"FIELD_HEADER_SAML" : "",
"INFO_SAML_REDIRECT_PENDING" : "Por favor aguarde, redirecionando para o provedor de indentidade..."
}
}