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,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验证..."
}
}