diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java index 59c6ebdec..a80ba82ab 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java @@ -63,6 +63,7 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS /** * The standard HTTP parameter which will be included within the URL by all * OpenID services upon successful implicit flow authentication. + * */ public static final String IMPLICIT_TOKEN_PARAMETER_NAME = "id_token"; @@ -136,13 +137,13 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS Set groups = null; Map tokens = Collections.emptyMap(); - logger.debug("OpenID authentication with {} flow (ID: {}, Secret: {}, PKCE: {})", - confService.getFlowType(), + logger.debug("OpenID authentication with '{}' reponse type (ID: {}, Secret: {}, PKCE: {})", + confService.getResponseType(), confService.getClientID(), confService.getClientSecret(), confService.isPKCERequired()); - if (confService.getFlowType().equals("implicit")) { + if (confService.isImplicitFlow()) { String token = credentials.getParameter(IMPLICIT_TOKEN_PARAMETER_NAME); if (token != null) { JwtClaims claims = tokenService.validateToken(token); @@ -194,21 +195,15 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS @Override public URI getLoginURI() throws GuacamoleException { - if (confService.getFlowType().equals("implicit")) { - return UriBuilder.fromUri(confService.getAuthorizationEndpoint()) - .queryParam("scope", confService.getScope()) - .queryParam("response_type", IMPLICIT_TOKEN_PARAMETER_NAME) - .queryParam("client_id", confService.getClientID()) - .queryParam("redirect_uri", confService.getRedirectURI()) - .queryParam("nonce", nonceService.generate(confService.getMaxNonceValidity() * 60000L)) - .build(); - } else { - UriBuilder builder = UriBuilder.fromUri(confService.getAuthorizationEndpoint()) - .queryParam("scope", confService.getScope()) - .queryParam("response_type", CODE_TOKEN_PARAMETER_NAME) - .queryParam("client_id", confService.getClientID()) - .queryParam("redirect_uri", confService.getRedirectURI()); + UriBuilder builder = UriBuilder.fromUri(confService.getAuthorizationEndpoint()) + .queryParam("scope", confService.getScope()) + .queryParam("response_type", confService.getResponseType().toString()) + .queryParam("client_id", confService.getClientID()) + .queryParam("redirect_uri", confService.getRedirectURI()); + if (confService.isImplicitFlow()) { + builder.queryParam("nonce", nonceService.generate(confService.getMaxNonceValidity() * 60000L)); + } else { if (confService.isPKCERequired()) { String codeVerifier = PKCEUtil.generateCodeVerifier(); String codeChallenge; @@ -230,9 +225,9 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS .queryParam("code_challenge_method", "S256") .queryParam(AUTH_SESSION_QUERY_PARAM, identifier); } - - return builder.build(); } + + return builder.build(); } @Override diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java index bbd5c4c34..a74832ab7 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java @@ -21,8 +21,10 @@ package org.apache.guacamole.auth.openid; import com.google.inject.AbstractModule; import com.google.inject.Scopes; +import com.google.inject.Singleton; import org.apache.guacamole.auth.openid.conf.ConfigurationService; import org.apache.guacamole.auth.openid.conf.OpenIDEnvironment; +import org.apache.guacamole.auth.openid.conf.OpenIDWellKnown; import org.apache.guacamole.auth.openid.OpenIDAuthenticationSessionManager; import org.apache.guacamole.auth.sso.NonceService; import org.apache.guacamole.auth.openid.token.TokenValidationService; diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java index 481352450..0b33437fa 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java @@ -20,13 +20,17 @@ package org.apache.guacamole.auth.openid.conf; import com.google.inject.Inject; + import java.net.URI; import java.util.Collection; import java.util.Collections; import java.util.List; +import org.apache.guacamole.auth.openid.conf.OpenIDResponseType; +import org.apache.guacamole.auth.openid.conf.OpenIDWellKnown; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.properties.BooleanGuacamoleProperty; +import org.apache.guacamole.properties.EnumGuacamoleProperty; import org.apache.guacamole.properties.IntegerGuacamoleProperty; import org.apache.guacamole.properties.StringGuacamoleProperty; import org.apache.guacamole.properties.URIGuacamoleProperty; @@ -38,9 +42,9 @@ import org.apache.guacamole.properties.URIGuacamoleProperty; public class ConfigurationService { /** - * The default OICD flow type + * The default OICD reponse type */ - private static final String DEFAULT_FLOW_TYPE = "implicit"; + private static final OpenIDResponseType DEFAULT_RESPONSE_TYPE = OpenIDResponseType.ID_TOKEN; /** * The default claim type to use to retrieve an authenticated user's @@ -128,13 +132,13 @@ public class ConfigurationService { }; /** - * The flow type of the OpenID service. + * The reponse type of the OpenID service. */ - private static final StringGuacamoleProperty OPENID_FLOW_TYPE = - new StringGuacamoleProperty() { + private static final EnumGuacamoleProperty OPENID_RESPONSE_TYPE = + new EnumGuacamoleProperty(OpenIDResponseType.class) { @Override - public String getName() { return "openid-flow-type"; } + public String getName() { return "openid-response-type"; } }; @@ -291,6 +295,12 @@ public class ConfigurationService { */ @Inject private Environment environment; + + /** + * Service for retrieving OpenID well-known data. + */ + @Inject + private OpenIDWellKnown confWellKnown; /** * Returns the authorization endpoint (URI) of the OpenID service as @@ -301,11 +311,15 @@ public class ConfigurationService { * 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); + URI authorization_endpoint = environment.getProperty(OPENID_AUTHORIZATION_ENDPOINT); + authorization_endpoint = authorization_endpoint == null ? confWellKnown.getAuthorizationEndpoint() : authorization_endpoint; + if (authorization_endpoint == null) { + throw new GuacamoleException("Property openid-authorization-endpoint or openid-well-known-endpoint is required"); + } + return authorization_endpoint; } /** @@ -357,7 +371,12 @@ public class ConfigurationService { * is missing. */ public String getIssuer() throws GuacamoleException { - return environment.getRequiredProperty(OPENID_ISSUER); + String issuer = environment.getProperty(OPENID_ISSUER); + issuer = issuer == null ? confWellKnown.getIssuer() : issuer; + if (issuer == null) { + throw new GuacamoleException("Property openid-issuer or openid-well-known-endpoint is required"); + } + return issuer; } /** @@ -375,7 +394,12 @@ public class ConfigurationService { * property is missing. */ public URI getJWKSEndpoint() throws GuacamoleException { - return environment.getRequiredProperty(OPENID_JWKS_ENDPOINT); + URI jwks_uri = environment.getProperty(OPENID_JWKS_ENDPOINT); + jwks_uri = jwks_uri == null ? confWellKnown.getJWKSEndpoint() : jwks_uri; + if (jwks_uri == null) { + throw new GuacamoleException("Property openid-jwks-endpoint or openid-well-known-endpoint is required"); + } + return jwks_uri; } /** @@ -387,25 +411,60 @@ public class ConfigurationService { * guacamole.properties. * * @throws GuacamoleException - * If guacamole.properties cannot be parsed, or if the authorization + * If guacamole.properties cannot be parsed, or if the token * endpoint property is missing. */ public URI getTokenEndpoint() throws GuacamoleException { - return environment.getRequiredProperty(OPENID_TOKEN_ENDPOINT); + URI token_endpoint = environment.getProperty(OPENID_TOKEN_ENDPOINT); + token_endpoint = token_endpoint == null ? confWellKnown.getTokenEndpoint() : token_endpoint; + if (token_endpoint == null) { + throw new GuacamoleException("Property openid-token-endpoint or openid-well-known-endpoint is required"); + } + return token_endpoint; } - + /** - * Returns the flow type of the OpenID service as configured with guacamole.properties. + * Returns the well-known endpoint (URI) of the OIDC service as + * configured with guacamole.properties. * * @return - * The flow type of the OpenID service, as configured with guacamole.properties. Can - * be either 'implicit' or 'code'. + * The well-known endpoint of the OIDC service, as configured with + * guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed, or if the well-known + * endpoint property is missing. + */ + public URI getWellKnownEndpoint() throws GuacamoleException { + return confWellKnown.getWellKnownEndpoint(); + } + + /** + * Returns the reponse type of the OpenID service as configured with guacamole.properties. + * + * @return + * The reponse type of the OpenID service, as configured with guacamole.properties. Can + * be either 'id_token', 'token' or 'code'. * * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ - public String getFlowType() throws GuacamoleException { - return environment.getProperty(OPENID_FLOW_TYPE, DEFAULT_FLOW_TYPE); + public OpenIDResponseType getResponseType() throws GuacamoleException { + return environment.getProperty(OPENID_RESPONSE_TYPE, DEFAULT_RESPONSE_TYPE); + } + + /** + * Returns true if the response type defines an implict flow + * + * @return + * The whether implicit flow is used or not, as configured with guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public boolean isImplicitFlow() throws GuacamoleException { + OpenIDResponseType response_type = environment.getProperty(OPENID_RESPONSE_TYPE, DEFAULT_RESPONSE_TYPE); + return response_type != OpenIDResponseType.CODE; } /** diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/OpenIDResponseType.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/OpenIDResponseType.java new file mode 100644 index 000000000..845771ab7 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/OpenIDResponseType.java @@ -0,0 +1,67 @@ +/* + * 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 org.apache.guacamole.properties.EnumGuacamoleProperty.PropertyValue; + +/** + * This enum represents the valid OIDC reponse types that can be used + */ + public enum OpenIDResponseType { + /* + * Response type "id_token" used for implciit flow as specified in the OpenID standard + */ + @PropertyValue("id_token") + ID_TOKEN("id_token"), + + /* Response type "token" used for implicit flow by certain Identity Providers, notably + * AWS Cognito. This corresponds to the official OIDC response_type "id_token token" + * that returns both the "id_token" and "access_token" parameters + */ + @PropertyValue("token") + TOKEN("token"), + + /** + * Response type "code" used for code flow authentication + */ + @PropertyValue("code") + CODE("code"); + + /* + * The string value of the response type used + */ + public final String STRING_VALUE; + + /** + * Initializes the response type such that it is associated with the + * given string value. + * + * @param value + * The string value that will be associated with the enum value. + */ + private OpenIDResponseType(String value) { + this.STRING_VALUE = value; + } + + @Override + public String toString() { + return STRING_VALUE; + } +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/OpenIDWellKnown.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/OpenIDWellKnown.java new file mode 100644 index 000000000..108fd7ecc --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/OpenIDWellKnown.java @@ -0,0 +1,214 @@ +/* + * 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 com.google.inject.Singleton; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.Map; +import javax.ws.rs.core.UriBuilder; +import org.apache.guacamole.auth.openid.util.JsonUrlReader; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.environment.Environment; +import org.apache.guacamole.properties.URIGuacamoleProperty; +import org.jose4j.json.JsonUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class for retrieving well-known endpoint data. + */ +@Singleton +public class OpenIDWellKnown { + + /** + * Logger for this class. + */ + private final Logger logger = LoggerFactory.getLogger(OpenIDWellKnown.class); + + + /** + * The number of attempts to the well-known endpoint to get the values before giving up + */ + private static final int MAX_ATTEMPTS = 24; + + /** + * The delay between each attempt to well-known endpoint in seconds + */ + private static final long DELAY_SECONDS = 5; + + /** + * The detected issuer + */ + private static String issuer = null; + + /** + * The detected authorization edpoint + */ + private static URI authorization_endpoint = null; + + /** + * The detected token edpoint + */ + private static URI token_endpoint = null; + + /** + * The detected jwks_uri + */ + private static URI jwks_uri = null; + + /** + * The well-known endpoint (URI) of the OIDC service. + */ + private static final URIGuacamoleProperty OPENID_WELL_KNOWN_ENDPOINT = + new URIGuacamoleProperty() { + @Override + public String getName() { + return "openid-well-known-endpoint"; + } + }; + + /** + * Returns the well-known endpoint (URI) of the OIDC service as + * configured with guacamole.properties. + * + * @return + * The well-known endpoint of the OIDC service, as configured with + * guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed, or if the authorization + * endpoint property is missing. + */ + public URI getWellKnownEndpoint() throws GuacamoleException { + return environment.getProperty(OPENID_WELL_KNOWN_ENDPOINT); + } + + /** + * Returns the issuer to expect for all received ID tokens, as configured + * from the well_known endpoint. + * + * @return + * The issuer to expect for all received ID tokens, as returned by the + * well-known endpoint. + */ + public String getIssuer() { + return issuer; + } + + /** + * Returns the authorization endpoint (URI) of the OpenID service as + * configured from the well_known endpoint. + * + * @return + * The authorization endpoint of the OpenID service, as returned by the + * well-known endpoint. + */ + public URI getAuthorizationEndpoint() { + return authorization_endpoint; + } + + /** + * Returns the token endpoint (URI) of the OpenID service as + * configured from the well_known endpoint. + * + * @return + * The token endpoint of the OpenID service, as returned by the + * well-known endpoint. + */ + public URI getTokenEndpoint() { + return token_endpoint; + } + + /** + * Returns the endpoint (URI) of the JWKS service which defines how + * received ID tokens (JWTs) shall be validated, as configured from + * the well-known endpoint. + * + * @return + * The endpoint (URI) of the JWKS service which defines how received ID + * tokens (JWTs) shall be validated, as configured from the + * well-known endpoint. + */ + public URI getJWKSEndpoint() { + return jwks_uri; + } + + /** + * The Guacamole server environment. + */ + @Inject + private Environment environment; + + /* + * Creates an OpenIDWellKnown class that reads the json from an OIDC + * well-known endpoint and saves these values for later use. Use Guice + * to ensure environment exists before initializing. + */ + public OpenIDWellKnown() { + } + + @Inject + public void init() { + // Call to well-known endpoint might fail, so allow several tries before giving up + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + Runnable task = new Runnable() { + int attempts = 0; + + @Override + public void run() { + attempts++; + + try { + Map json = JsonUrlReader.fetch("GET", getWellKnownEndpoint().toURL(), ""); + issuer = (String) json.get("issuer"); + authorization_endpoint = UriBuilder.fromUri((String) json.get("authorization_endpoint")).build(); + token_endpoint = UriBuilder.fromUri((String) json.get("token_endpoint")).build(); + jwks_uri = UriBuilder.fromUri((String) json.get("jwks_uri")).build(); + + logger.debug("OIDC well-known\n" + + " issuer : {}\n" + + " authorization_endpoint : {}\n" + + " token_endpoint : {}\n" + + " jwks_uri : {}\n", + issuer, authorization_endpoint, token_endpoint, jwks_uri); + + scheduler.shutdown(); + return; + } catch (Exception e) { + logger.debug("Rejecting well-known endpoint : {}", e.getMessage()); + } + + if (attempts >= MAX_ATTEMPTS) { + logger.info("Timeout on well-known on endpoint"); + scheduler.shutdown(); + } + } + }; + + scheduler.scheduleAtFixedRate(task, 0, DELAY_SECONDS, TimeUnit.SECONDS); + } +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java index c0f4dfae4..6a7d3cf76 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java @@ -20,11 +20,6 @@ package org.apache.guacamole.auth.openid.token; import com.google.inject.Inject; -import java.io.BufferedReader; -import java.io.OutputStream; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; import java.net.URLEncoder; import java.net.URI; import java.nio.charset.StandardCharsets; @@ -39,6 +34,7 @@ import java.util.Set; import javax.ws.rs.core.UriBuilder; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.openid.conf.ConfigurationService; +import org.apache.guacamole.auth.openid.util.JsonUrlReader; import org.apache.guacamole.auth.sso.NonceService; import org.apache.guacamole.token.TokenName; import org.jose4j.json.JsonUtil; @@ -245,46 +241,9 @@ public class TokenValidationService { bodyBuilder.append("&").append(urlencode("code_verifier", verifier)); } - // Build the final URI and convert to a URL - URL url = confService.getTokenEndpoint().toURL(); - - // Open connection, using HttpURLConnection - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - - conn.setRequestMethod("POST"); - conn.setDoOutput(true); - conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8" - ); - - try (OutputStream out = conn.getOutputStream()) { - byte [] body = bodyBuilder.toString().getBytes(StandardCharsets.UTF_8); - out.write(body, 0, body.length); - } - - // Read response - int status = conn.getResponseCode(); - - BufferedReader reader = new BufferedReader( - new InputStreamReader( - status >= 200 && status < 300 - ? conn.getInputStream() - : conn.getErrorStream(), - StandardCharsets.UTF_8 - ) - ); - - StringBuilder responseBody = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - responseBody.append(line); - } - reader.close(); - - Map json = JsonUtil.parseJson(responseBody.toString()); - - if (status < 200 || status >= 300) { - throw new GuacamoleException("Token endpoint error (" + status + "): " + json.toString()); - } + Map json = + JsonUrlReader.fetch("POST", confService.getTokenEndpoint().toURL(), + bodyBuilder.toString()); return (String) json.get("id_token"); diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/util/JsonUrlReader.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/util/JsonUrlReader.java new file mode 100644 index 000000000..b24299865 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/util/JsonUrlReader.java @@ -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.openid.util; + +import java.io.BufferedReader; +import java.io.OutputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import org.apache.guacamole.GuacamoleException; +import org.jose4j.json.JsonUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/* + * Utility class to open a http connection to a URL, send a body + * and receive a response in the form of a parsed JSON + */ +public final class JsonUrlReader { + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(JsonUrlReader.class); + + private JsonUrlReader() {} + + public static Map fetch(String method, URL url, String body) throws GuacamoleException { + if (url == null) { + throw new GuacamoleException("JsonUrlReader : Missing URL"); + } + + try { + // Open connection, using HttpURLConnection + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + + conn.setRequestMethod(method); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + + if (! method.equalsIgnoreCase("GET")) { + conn.setDoOutput(true); + try (OutputStream out = conn.getOutputStream()) { + byte [] bodyBytes = body.getBytes(StandardCharsets.UTF_8); + out.write(bodyBytes, 0, bodyBytes.length); + } + } + + // Read response + int status = conn.getResponseCode(); + + BufferedReader reader = new BufferedReader( + new InputStreamReader( + status >= 200 && status < 300 + ? conn.getInputStream() + : conn.getErrorStream(), + StandardCharsets.UTF_8 + ) + ); + + StringBuilder responseBody = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + responseBody.append(line); + } + reader.close(); + + logger.debug("Response body : {}", responseBody.toString()); + + Map json = JsonUtil.parseJson(responseBody.toString()); + + if (status < 200 || status >= 300) { + throw new GuacamoleException("(status: " + status + "): " + json.toString()); + } + + return json; + } catch (Exception e) { + throw new GuacamoleException("JsonUrlreader error : " + method + " : " + e.getMessage()); + } + } +}