| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.apache.guacamole.auth.saml.acs; |
| |
| import com.google.inject.Inject; |
| import com.google.inject.Singleton; |
| import com.onelogin.saml2.authn.AuthnRequest; |
| import com.onelogin.saml2.authn.SamlResponse; |
| import com.onelogin.saml2.exception.SettingsException; |
| import com.onelogin.saml2.exception.ValidationError; |
| import com.onelogin.saml2.settings.Saml2Settings; |
| import java.io.IOException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import javax.ws.rs.core.UriBuilder; |
| import javax.xml.parsers.ParserConfigurationException; |
| import javax.xml.xpath.XPathExpressionException; |
| import org.apache.guacamole.GuacamoleException; |
| import org.apache.guacamole.GuacamoleSecurityException; |
| import org.apache.guacamole.GuacamoleServerException; |
| import org.apache.guacamole.auth.saml.conf.ConfigurationService; |
| import org.xml.sax.SAXException; |
| |
| /** |
| * Service which abstracts the internals of handling SAML requests and |
| * responses. |
| */ |
| @Singleton |
| public class SAMLService { |
| |
| /** |
| * Service for retrieving SAML configuration information. |
| */ |
| @Inject |
| private ConfigurationService confService; |
| |
| /** |
| * Manager of active SAML authentication attempts. |
| */ |
| @Inject |
| private AuthenticationSessionManager sessionManager; |
| |
| /** |
| * Creates a new SAML request, beginning the overall authentication flow |
| * that will ultimately result in an asserted user identity if the user is |
| * successfully authenticated by the SAML IdP. The URI of the SSO endpoint |
| * of the SAML IdP that the user must be redirected to for the |
| * authentication process to continue is returned. |
| * |
| * @return |
| * The URI of the SSO endpoint of the SAML IdP that the user must be |
| * redirected to. |
| * |
| * @throws GuacamoleException |
| * If an error prevents the SAML request and redirect URI from being |
| * generated. |
| */ |
| public URI createRequest() throws GuacamoleException { |
| |
| Saml2Settings samlSettings = confService.getSamlSettings(); |
| AuthnRequest samlReq = new AuthnRequest(samlSettings); |
| |
| // Create a new authentication session to represent this attempt while |
| // it is in progress |
| AuthenticationSession session = new AuthenticationSession(samlReq.getId(), |
| confService.getAuthenticationTimeout() * 60000L); |
| |
| // Produce redirect for continuing the authentication process with |
| // the SAML IdP |
| try { |
| return UriBuilder.fromUri(samlSettings.getIdpSingleSignOnServiceUrl().toURI()) |
| .queryParam("SAMLRequest", samlReq.getEncodedAuthnRequest()) |
| .queryParam("RelayState", sessionManager.defer(session)) |
| .build(); |
| } |
| catch (IOException e) { |
| throw new GuacamoleServerException("SAML authentication request " |
| + "could not be encoded: " + e.getMessage()); |
| } |
| catch (URISyntaxException e) { |
| throw new GuacamoleServerException("SAML IdP redirect could not " |
| + "be generated due to an error in the URI syntax: " |
| + e.getMessage()); |
| } |
| |
| } |
| |
| /** |
| * Processes the given SAML response, as received by the SAML ACS endpoint |
| * at the given URL, producing an {@link AuthenticationSession} that now |
| * includes a valid assertion of the user's identity. If the SAML response |
| * is invalid in any way, an exception is thrown. |
| * |
| * @param url |
| * The URL of the ACS endpoint that received the SAML response. This |
| * should be the URL pointing to the single POST-handling endpoint of |
| * {@link AssertionConsumerServiceResource}. |
| * |
| * @param relayState |
| * The "RelayState" value originally provided in the SAML request, |
| * which in our case is the transient the session identifier of the |
| * in-progress authentication attempt. The SAML standard requires that |
| * the identity provider include the "RelayState" value it received |
| * alongside its SAML response. |
| * |
| * @param encodedResponse |
| * The response received from the SAML IdP via the ACS endpoint at the |
| * given URL. |
| * |
| * @return |
| * The {@link AuthenticationSession} associated with the in-progress |
| * authentication attempt, now associated with the {@link AssertedIdentity} |
| * representing the identity of the user asserted by the SAML IdP. |
| * |
| * @throws GuacamoleException |
| * If the given SAML response is not valid, or if the configuration |
| * information required to validate or decrypt the response cannot be |
| * read. |
| */ |
| public AuthenticationSession processResponse(String url, String relayState, |
| String encodedResponse) throws GuacamoleException { |
| |
| if (relayState == null) |
| throw new GuacamoleSecurityException("\"RelayState\" value " |
| + "is missing from SAML response."); |
| |
| AuthenticationSession session = sessionManager.resume(relayState); |
| if (session == null) |
| throw new GuacamoleSecurityException("\"RelayState\" value " |
| + "included with SAML response is not valid."); |
| |
| try { |
| |
| // Decode received SAML response |
| SamlResponse response = new SamlResponse(confService.getSamlSettings(), |
| url, encodedResponse); |
| |
| // Validate SAML response timestamp, signature, etc. |
| if (!response.isValid(session.getRequestID())) { |
| Exception validationException = response.getValidationException(); |
| throw new GuacamoleSecurityException("SAML response did not " |
| + "pass validation: " + validationException.getMessage(), |
| validationException); |
| } |
| |
| // Parse identity asserted by SAML IdP |
| session.setIdentity(new AssertedIdentity(response)); |
| return session; |
| |
| } |
| catch (ValidationError e) { |
| throw new GuacamoleSecurityException("SAML response did not pass " |
| + "validation: " + e.getMessage(), e); |
| } |
| catch (SettingsException e) { |
| throw new GuacamoleServerException("Current SAML settings are " |
| + "insufficient to decrypt/parse the received SAML " |
| + "response.", e); |
| } |
| catch (ParserConfigurationException | SAXException | XPathExpressionException e) { |
| throw new GuacamoleServerException("XML contents of SAML " |
| + "response could not be parsed.", e); |
| } |
| catch (IOException e) { |
| throw new GuacamoleServerException("Contents of SAML response " |
| + "could not be decrypted/read.", e); |
| } |
| |
| } |
| |
| } |