diff --git a/doc/licenses/com.auth0:java-jwt:jar:4.3.0/LICENSE b/doc/licenses/com.auth0:java-jwt:jar:4.3.0/LICENSE new file mode 100644 index 0000000000..bcd1854c48 --- /dev/null +++ b/doc/licenses/com.auth0:java-jwt:jar:4.3.0/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Auth0, Inc. (http://auth0.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doc/licenses/com.auth0:java-jwt:jar:4.3.0/README b/doc/licenses/com.auth0:java-jwt:jar:4.3.0/README new file mode 100644 index 0000000000..c3ced71f2b --- /dev/null +++ b/doc/licenses/com.auth0:java-jwt:jar:4.3.0/README @@ -0,0 +1,7 @@ +Auth0 JWT (https://github.com/auth0/java-jwt) +---------------------------------------------------------- + + Version: 4.3.0 + From: 'Okta' + License(s): + MIT diff --git a/doc/licenses/com.auth0:java-jwt:jar:4.3.0/dep-coordinates.txt b/doc/licenses/com.auth0:java-jwt:jar:4.3.0/dep-coordinates.txt new file mode 100644 index 0000000000..ffa53d956c --- /dev/null +++ b/doc/licenses/com.auth0:java-jwt:jar:4.3.0/dep-coordinates.txt @@ -0,0 +1 @@ +com.auth0:java-jwt:jar:4.3.0 diff --git a/extensions/guacamole-auth-nextcloud/.ratignore b/extensions/guacamole-auth-nextcloud/.ratignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/extensions/guacamole-auth-nextcloud/doc/README.md b/extensions/guacamole-auth-nextcloud/doc/README.md new file mode 100644 index 0000000000..bfa20182db --- /dev/null +++ b/extensions/guacamole-auth-nextcloud/doc/README.md @@ -0,0 +1,46 @@ +guacamole-auth-nextcloud +=================== + +guacamole-auth-nextcloud is an authentication extension for [Apache +Guacamole](http://guacamole.apache.org/) that authenticates users using +a JWT. This token will be generated by the Nextcloud extension [External +sites](https://apps.nextcloud.com/apps/external) and will be sent +automatically when the Guacamole login page is integrated into your +Nextcloud. + +Prepare your Nextcloud +---------------------- + +Make sure that the [External sites](https://apps.nextcloud.com/apps/external) +plugin is already installed. Use the following [occ](https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/occ_command.html) +command to get the required public key. Remove all line breaks and +comments and add it to your properties file. + + $ occ config:app:get external jwt_token_pubkey_es256 + +Now you have to add a new website in the plugin settings. There you have +to define a URL, which can look like this: + + https://my-nextcloud.com/guacamole/?nctoken={jwt} + +The GET parameter can have any name, by default it is `nctoken`. If you +want to set a different name, you must also set it in the properties file. + +Properties +----------- + +Property name | Type | Description +--------------|----------|------------ +`nextcloud-jwt-token-name` | `string` | The key of the GET parameter which contains the JWT token as a value. Default is `nctoken`. +`nextcloud-jwt-allowed-user` | `string` | An optional list of Nextcloud users who are allowed to open the login page with a valid JWT. Make sure that you use the `uid` and not the displayed name. If the list is empty, all users are allowed, but they still need a valid JWT. +`nextcloud-jwt-public-key` | `string` | The public key which will be visible with the `occ` command in your Nextcloud instance via the console. Insert the key without line breaks and comments. +`nextcloud-jwt-trusted-networks` | `string` | An optional comma-separated list of permitted IP addresses. An empty list means that all IP addresses are permitted without JWT validation. If the request comes from a trusted network (e.g. local network), the JWT validation will be skipped. The users must authenticate normally with their Guacamole username and password. + +Example +------- + + nextcloud-jwt-token-name: mytoken + nextcloud-jwt-allowed-user: JohnDoe,JaneDoe + nextcloud-jwt-public-key: MFkwEwYHKoZIzj0....st5CuQ== + nextcloud-jwt-trusted-networks: 10.8.0.27,10.8.0.41 + diff --git a/extensions/guacamole-auth-nextcloud/pom.xml b/extensions/guacamole-auth-nextcloud/pom.xml new file mode 100644 index 0000000000..b87b92b6cf --- /dev/null +++ b/extensions/guacamole-auth-nextcloud/pom.xml @@ -0,0 +1,98 @@ + + + + + 4.0.0 + org.apache.guacamole + guacamole-auth-nextcloud + jar + 1.5.5 + guacamole-auth-nextcloud + http://guacamole.apache.org/ + + + org.apache.guacamole + extensions + 1.5.5 + ../ + + + + + + + org.apache.guacamole + guacamole-ext + 1.5.5 + provided + + + + + com.google.inject + guice + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.google.guava + guava + + + + + javax.servlet + servlet-api + 2.5 + provided + + + + + com.github.seancfoley + ipaddress + 5.5.0 + + + + com.auth0 + java-jwt + 4.3.0 + + + + + junit + junit + test + + + + + diff --git a/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/ConfigurationService.java b/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/ConfigurationService.java new file mode 100644 index 0000000000..36a6d30a77 --- /dev/null +++ b/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/ConfigurationService.java @@ -0,0 +1,162 @@ +/* + * 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.nextcloud; + +import com.google.inject.Inject; +import java.util.Collection; +import java.util.Collections; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.environment.Environment; +import org.apache.guacamole.properties.StringGuacamoleProperty; +import org.apache.guacamole.properties.StringListProperty; + +/** + * Service for retrieving configuration information regarding the Nextcloud JWT + * authentication provider. + */ +public class ConfigurationService { + + /** + * The Guacamole server environment. + */ + @Inject + private Environment environment; + + /** + * The encryption key to use for all decryption and signature verification. + */ + private static final StringGuacamoleProperty NEXTCLOUD_JWT_PUBLIC_KEY = new StringGuacamoleProperty() { + @Override + public String getName() { + return "nextcloud-jwt-public-key"; + } + + }; + + /** + * A comma-separated list of all IP addresses or CIDR subnets which should + * be allowed to perform authentication. If not specified, ALL address will + * be allowed. + */ + private static final StringListProperty NEXTCLOUD_JWT_TRUSTED_NETWORKS = new StringListProperty() { + + @Override + public String getName() { + return "nextcloud-jwt-trusted-networks"; + } + + }; + + /** + * Property for retrieving the list of users allowed to authenticate via JWT. + * + * This property defines a configuration setting that specifies the users permitted + * to use JWT for authentication. + */ + private static final StringListProperty NEXTCLOUD_JWT_ALLOWED_USER = new StringListProperty() { + + @Override + public String getName() { + return "nextcloud-jwt-allowed-user"; + } + + }; + + /** + * Property for retrieving the name of the token used for authentication. + * + * This property defines a configuration setting that specifies the name of the + * token to be used for authentication purposes. The key of the GET parameter is + * defined by this property, and the value of the GET parameter contains the token. + */ + private static final StringGuacamoleProperty NEXTCLOUD_JWT_TOKEN_NAME = new StringGuacamoleProperty() { + @Override + public String getName() { + return "nextcloud-jwt-token-name"; + } + + }; + + /** + * Returns the symmetric key which will be used to encrypt and sign all + * JSON data and should be used to decrypt and verify any received JSON + * data. This is dictated by the "nextcloud-jwt-public-key" property specified + * within guacamole.properties. + * + * @return + * The key which should be used to decrypt received JSON data. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed, or if the + * "nextcloud-jwt-public-key" property is missing. + */ + public String getPublicKey() throws GuacamoleException { + return environment.getRequiredProperty(NEXTCLOUD_JWT_PUBLIC_KEY); + } + + /** + * Returns a collection of all IP address or CIDR subnets which should be + * allowed to submit authentication requests. If empty, authentication + * attempts will be allowed through without restriction. + * + * @return + * A collection of all IP address or CIDR subnets which should be + * allowed to submit authentication requests. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public Collection getTrustedNetworks() throws GuacamoleException { + return environment.getProperty(NEXTCLOUD_JWT_TRUSTED_NETWORKS, Collections.emptyList()); + } + + /** + * Retrieves the collection of users allowed to authenticate via JWT. + * + * This method fetches the list of allowed users from the environment properties. + * If the property is not set, it returns an empty list. + * + * @return + * A collection of allowed user identifiers. + * + * @throws GuacamoleException + * If there is an issue retrieving the property. + */ + public Collection getAllowedUser() throws GuacamoleException { + return environment.getProperty(NEXTCLOUD_JWT_ALLOWED_USER, Collections.emptyList()); + } + + /** + * Retrieves the name of the token used for authentication. + * + * This method fetches the token name from the environment properties. + * If the property is not set, it returns the default value "nctoken". + * + * @return + * The name of the token used for authentication. + * + * @throws GuacamoleException + * If there is an issue retrieving the property. + */ + public String getTokenName() throws GuacamoleException { + return environment.getProperty(NEXTCLOUD_JWT_TOKEN_NAME, "nctoken"); + } + +} diff --git a/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/NextcloudJwtAuthenticationProvider.java b/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/NextcloudJwtAuthenticationProvider.java new file mode 100644 index 0000000000..1bb470d2d3 --- /dev/null +++ b/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/NextcloudJwtAuthenticationProvider.java @@ -0,0 +1,317 @@ +/* + * 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.nextcloud; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.ECPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.Date; +import javax.servlet.http.HttpServletRequest; +import org.apache.guacamole.GuacamoleClientException; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleSecurityException; +import org.apache.guacamole.net.auth.AbstractAuthenticationProvider; +import org.apache.guacamole.auth.nextcloud.user.AuthenticatedUser; +import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.token.GuacamoleTokenUndefinedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Allows a pre-check of users with encrypted Nextcloud JWT data blocks. + * The username in the JWT will be compared with a list in guacamole.properties. + * The JWT will be verified with the public key. If the JWT is valid, the login + * page will be loaded. If the JWT is missing or invalid, an exception message + * will be displayed. + */ +public class NextcloudJwtAuthenticationProvider extends AbstractAuthenticationProvider { + + /** + * The duration in minutes for which a token remains valid. + * + *

This short validity period increases security, as the time window for potential misuse, + * e.g. by stolen tokens, is limited. Nextcloud always generates a new valid token when the + * Guacamole login screen will be open through the Nextcloud plugin “External sites”. + */ + private static final int MINUTES_TOKEN_VALID = 1; + + /** + * Injector which will manage the object graph of this authentication + * provider. + */ + private final Injector injector; + + /** + * The configuration service for this module. + */ + @Inject + private ConfigurationService confService; + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(NextcloudJwtAuthenticationProvider.class); + + /** + * Creates a new NextcloudJwtAuthenticationProvider that authenticates user. + * + * @throws GuacamoleException + * If a required property is missing, or an error occurs while parsing + * a property. + */ + public NextcloudJwtAuthenticationProvider() throws GuacamoleException { + + // Set up Guice injector. + injector = Guice.createInjector(new NextcloudJwtAuthenticationProviderModule(this)); + + } + + @Override + public String getIdentifier() { + return "nextcloud"; + } + + /** + * Authenticates a user based on the provided credentials using a JWT from a Nextcloud instance. + * + *

This method performs the following steps to authenticate a user: + *

    + *
  • Retrieves the HTTP request and extracts the token and IP address.
  • + *
  • Checks if the request comes from a trusted IP address. If this is the case, JWT authentication will be + * skipped.
  • + *
  • Verifies that the token is present in the request.
  • + *
  • Decodes and validates the JWT using the configured public key.
  • + *
  • Extracts the UID (User ID) from the JWT payload and check if the user is allowed to access the + * application.
  • + *
+ * + * @param credentials + * The credentials which are in the HTTP request. + * + * @return an {@code AuthenticatedUser} object + * If the user is successfully authenticated; {@code null} if the request is from a trusted IP address. + * + * @throws GuacamoleException + * If authentication fails if the token is missing, expired or the user is not authorized. + */ + @Override + public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException { + + // Retrieve the HTTP request and extract the token and ip address. + HttpServletRequest request = credentials.getRequest(); + String token = request.getParameter(confService.getTokenName()); + String ipaddr = request.getRemoteAddr(); + + // If the request from ip address is allowed, jwt authentication is not required. + boolean localAddr = validIpAddress(ipaddr); + if (localAddr) { + logger.info("Request from local address {}", ipaddr); + return null; + } + + // Authentication fail if the token is not present or has not been found. + if (token == null) { + logger.error("Missing token '{}'.", confService.getTokenName()); + throw new GuacamoleTokenUndefinedException("Missing token.", confService.getTokenName()); + } + + try { + // Decode the Base64 encoded public key from configuration and generate the ECPublicKey object. + DecodedJWT decodedJWT = getDecodedJWT(token); + validateJwt(decodedJWT); + + // Check if the user extracted from the token's payload is allowed to open Guacamole login page. + String uid = getUserId(decodedJWT.getPayload()); + if (!isUserAllowed(uid)) { + logger.warn("User '{}' not allowed.", uid); + throw new GuacamoleSecurityException("User not allowed."); + } + + return new AuthenticatedUser(uid, this, credentials); + } + catch (JsonProcessingException ex) { + logger.error("JSON processing error occurred.", ex); + throw new GuacamoleClientException(ex.getMessage()); + } + catch (NoSuchAlgorithmException | InvalidKeySpecException ex) { + logger.error("An error has occurred during JWT decoding", ex); + throw new GuacamoleSecurityException(ex.getMessage()); + } + } + + /** + * Validates a JSON Web Token (JWT) for expiration. + * + *

This method checks if the decoded JWT is still valid by comparing the current date + * with the token's expiration date. The token must expire within an acceptable validity + * duration defined by {@code MINUTES_TOKEN_VALID}. + * + * @param decodedJWT + * The decoded JWT to validate. + * + * @throws GuacamoleException + * If the token is expired. + */ + private void validateJwt(DecodedJWT decodedJWT) throws GuacamoleException { + // Validate the token's expiration by comparing the current date with the token's expiration date, + // ensuring it falls within the acceptable validity duration defined by MINUTES_TOKEN_VALID. + Date currentDate = new Date(); + Date maxValidDate = new Date(currentDate.getTime() - (MINUTES_TOKEN_VALID * 60 * 1000)); + boolean isValidToken = decodedJWT.getExpiresAt().after(maxValidDate); + if (!isValidToken) { + throw new GuacamoleSecurityException("Token expired."); + } + } + + /** + * Decodes a JSON Web Token (JWT) using the public key configured in the service. + * + *

This method decodes a JWT by verifying it with an elliptic curve public key + * fetched from the configuration service. The public key is decoded from Base64 + * and used to create a verifier instance which then verifies and decodes the JWT. + * + * @param token + * The JWT token to decode. + * + * @return + * The decoded JWT. + * + * @throws GuacamoleException + * If there is an error in the configuration service. + * + * @throws NoSuchAlgorithmException + * If the algorithm for the key factory is not available. + * + * @throws InvalidKeySpecException + * If the provided key specification is invalid. + */ + private DecodedJWT getDecodedJWT(String token) throws GuacamoleException, NoSuchAlgorithmException, + InvalidKeySpecException { + + byte[] keyBytes = Base64.getDecoder().decode(confService.getPublicKey()); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); + ECPublicKey publicKey = (ECPublicKey) keyFactory.generatePublic(keySpec); + + // Create a JWT verifier instance with the provided public key, verify the token and decode the content. + JWTVerifier verifier = JWT.require(Algorithm.ECDSA256(publicKey)).build(); + DecodedJWT decodedJWT = verifier.verify(token); + + return decodedJWT; + } + + /** + * Validates whether an IP address is allowed based on the configured trusted networks. + *

+ * This method checks if the given IP address is within the range of any configured trusted networks. + * If the list of trusted networks is empty, the method returns {@code true}, indicating that all IP addresses + * are allowed. + *

+ * + * @param ipAddress + * The IP address to validate. + * + * @return {@code true} + * If the IP address is allowed or if the list of trusted networks is empty; {@code false} otherwise. + * + * @throws GuacamoleException + * If an error occurs while accessing the configuration service. + */ + private boolean validIpAddress(String ipAddress) throws GuacamoleException { + + // allow all ip addresses if not restricted + if (confService.getTrustedNetworks().isEmpty()) { + logger.info("No IP addresses defined. All IP addresses are allowed."); + return true; + } + + if (confService.getTrustedNetworks().contains(ipAddress)) { + logger.info("{} in list of allowed IP addresses.", ipAddress); + return true; + } + + logger.warn("{} not in list of allowed IP addresses.", ipAddress); + return false; + } + + /** + * Checks if a user is allowed based on the user ID. + * + *

This method verifies if the specified user ID is present in the list of allowed users. + * If the list of allowed users is empty, it permits all users. + * + * @param uid + * The user ID to check for permission. + * + * @return {@code true} + * If the user is allowed, {@code false} otherwise. + * + * @throws GuacamoleException + * If there is an issue retrieving the property 'nextcloud-jwt-allowed-user'. + */ + private boolean isUserAllowed(String uid) throws GuacamoleException { + + // allow all users if not restricted + if (confService.getAllowedUser().isEmpty()) { + logger.info("No users defined. All users are allowed."); + return true; + } + + return confService.getAllowedUser().contains(uid); + } + + /** + * Decodes a Base64 encoded JSON payload and extracts the uid + * + *

This method takes a Base64 encoded string as input, decodes it to a JSON string, + * parses the JSON to extract the user ID from the "userdata" object. + * + * @param payload + * The Base64 encoded JSON string containing user data. + * + * @return + * The user ID extracted from the decoded JSON payload. + * + * @throws JsonProcessingException + * If there is an error processing the JSON payload. + */ + private String getUserId(String payload) throws JsonProcessingException { + byte[] decodedBytes = Base64.getDecoder().decode(payload); + String decodedPayload = new String(decodedBytes, StandardCharsets.UTF_8); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode payloadJson = objectMapper.readTree(decodedPayload); + + return payloadJson.get("userdata").get("uid").asText(); + } +} diff --git a/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/NextcloudJwtAuthenticationProviderModule.java b/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/NextcloudJwtAuthenticationProviderModule.java new file mode 100644 index 0000000000..c0df334d4a --- /dev/null +++ b/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/NextcloudJwtAuthenticationProviderModule.java @@ -0,0 +1,80 @@ +/* + * 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.nextcloud; + +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; + +/** + * Guice module which configures injections specific to the Nextcloud JWT authentication + * provider. + */ +public class NextcloudJwtAuthenticationProviderModule extends AbstractModule { + + /** + * Guacamole server environment. + */ + private final Environment environment; + + /** + * A reference to the NextcloudJwtAuthenticationProvider on behalf of which this + * module has configured injection. + */ + private final AuthenticationProvider authProvider; + + /** + * Creates a new Nextcloud JWT authentication provider module which configures + * injection for the NextcloudJwtAuthenticationProvider. + * + * @param authProvider + * The AuthenticationProvider for which injection is being configured. + * + * @throws GuacamoleException + * If an error occurs while retrieving the Guacamole server + * environment. + */ + public NextcloudJwtAuthenticationProviderModule(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 NextcloudJwt-specific services + bind(ConfigurationService.class); + bind(RequestValidationService.class); + + } + +} diff --git a/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/NextcloudJwtUserContext.java b/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/NextcloudJwtUserContext.java new file mode 100644 index 0000000000..c2dab4e510 --- /dev/null +++ b/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/NextcloudJwtUserContext.java @@ -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.nextcloud; + +import java.util.Collections; +import org.apache.guacamole.net.auth.AuthenticationProvider; +import org.apache.guacamole.net.auth.ConnectionGroup; +import org.apache.guacamole.net.auth.simple.SimpleConnectionGroup; +import org.apache.guacamole.net.auth.simple.SimpleUserContext; + +public class NextcloudJwtUserContext extends SimpleUserContext { + + public NextcloudJwtUserContext(AuthenticationProvider authProvider, String username) { + super(authProvider, username, Collections.emptyMap()); + } + + @Override + public ConnectionGroup getRootConnectionGroup() { + // Return an empty root connection group + return new SimpleConnectionGroup( + "ROOT", + "ROOT", + Collections.emptyList(), + Collections.emptyList() + ); + } +} diff --git a/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/RequestValidationService.java b/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/RequestValidationService.java new file mode 100644 index 0000000000..7b3bbfe99d --- /dev/null +++ b/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/RequestValidationService.java @@ -0,0 +1,108 @@ +/* + * 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.nextcloud; + +import com.google.inject.Inject; +import java.util.Collection; +import javax.servlet.http.HttpServletRequest; +import inet.ipaddr.IPAddressString; +import org.apache.guacamole.GuacamoleException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Service for testing the validity of received HTTP requests. + */ +public class RequestValidationService { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(RequestValidationService.class); + + /** + * Service for retrieving configuration information regarding the + * NextcloudJwtAuthenticationProvider. + */ + private ConfigurationService confService; + + /** + * Create a new instance of the request validation service, with the + * provided ConfigurationService object used to retrieve configuration + * properties for this extension. + * + * @param confService + * The instance of ConfigurationService for retrieving configuration + * properties for this extension. + */ + @Inject + public RequestValidationService(ConfigurationService confService) { + this.confService = confService; + } + + /** + * Returns whether the given request can be used for authentication, taking + * into account restrictions specified within guacamole.properties. + * + * @param request + * The HTTP request to test. + * + * @return + * true if the given request comes from a trusted source and can be + * used for authentication, false otherwise. + */ + public boolean isAuthenticationAllowed(HttpServletRequest request) { + + // Pull list of all trusted networks + Collection trustedNetworks; + try { + trustedNetworks = confService.getTrustedNetworks(); + } + // Deny all requests if restrictions cannot be parsed + catch (GuacamoleException e) { + logger.warn("Authentication request from \"{}\" is DENIED due to parse error: {}", request.getRemoteAddr(), e.getMessage()); + logger.debug("Error parsing authentication request restrictions from guacamole.properties.", e); + return false; + } + + // All requests are allowed if no restrictions are defined + if (trustedNetworks.isEmpty()) { + logger.debug("Authentication request from \"{}\" is ALLOWED (no restrictions).", request.getRemoteAddr()); + return true; + } + + // Otherwise ensure that the remote address is part of a trusted network + for (String network : trustedNetworks) { + + // Request is allowed if any subnet matches + if (new IPAddressString(network).contains(new IPAddressString(request.getRemoteAddr()))) { + logger.debug("Authentication request from \"{}\" is ALLOWED (matched subnet).", request.getRemoteAddr()); + return true; + } + + } + + // Otherwise request is denied - no subnets matched + logger.debug("Authentication request from \"{}\" is DENIED (did not match subnet).", request.getRemoteAddr()); + return false; + + } + +} diff --git a/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/user/AuthenticatedUser.java b/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/user/AuthenticatedUser.java new file mode 100644 index 0000000000..bafb9ae9e5 --- /dev/null +++ b/extensions/guacamole-auth-nextcloud/src/main/java/org/apache/guacamole/auth/nextcloud/user/AuthenticatedUser.java @@ -0,0 +1,96 @@ +/* + * 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.nextcloud.user; + +import com.google.inject.Inject; +import java.util.Collections; +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; + +/** + * A Nextcloud JWT implementation of AuthenticatedUser, associating a + * username and particular set of credentials with the HTTP 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 UID (User ID) which is defined inside the JWT payload. + */ + private String identifier; + + /** + * Constructs an {@code AuthenticatedUser} with the specified identifier, authentication provider, and credentials. + * + * @param identifier + * The unique identifier for the authenticated user. + * + * @param authProvider + * The authentication provider used for this user. + * + * @param credentials + * The credentials of HTTP request. + */ + public AuthenticatedUser(String identifier, AuthenticationProvider authProvider, Credentials credentials) { + this.identifier = identifier; + this.authProvider = authProvider; + this.credentials = credentials; + } + + @Override + public String getIdentifier() { + return identifier; + } + + @Override + public AuthenticationProvider getAuthenticationProvider() { + return authProvider; + } + + @Override + public Credentials getCredentials() { + return credentials; + } + + @Override + public Set getEffectiveUserGroups() { + return Collections.emptySet(); + } + + @Override + public void invalidate() { + // No invalidation logic needed + } + +} diff --git a/extensions/guacamole-auth-nextcloud/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-nextcloud/src/main/resources/guac-manifest.json new file mode 100644 index 0000000000..671a594355 --- /dev/null +++ b/extensions/guacamole-auth-nextcloud/src/main/resources/guac-manifest.json @@ -0,0 +1,12 @@ +{ + + "guacamoleVersion" : "1.5.5", + + "name" : "Encrypted Nextcloud Authentication", + "namespace" : "nextcloud", + + "authProviders" : [ + "org.apache.guacamole.auth.nextcloud.NextcloudJwtAuthenticationProvider" + ] + +} diff --git a/extensions/pom.xml b/extensions/pom.xml index 2e8fca026e..9dd0b578ac 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -46,6 +46,7 @@ guacamole-auth-jdbc guacamole-auth-json guacamole-auth-ldap + guacamole-auth-nextcloud guacamole-auth-quickconnect guacamole-auth-sso guacamole-auth-totp