package com.onelogin.saml2;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.Instant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.onelogin.saml2.authn.AuthnRequest;
import com.onelogin.saml2.authn.SamlResponse;
import com.onelogin.saml2.exception.SettingsException;
import com.onelogin.saml2.exception.Error;
import com.onelogin.saml2.exception.XMLEntityException;
import com.onelogin.saml2.http.HttpRequest;
import com.onelogin.saml2.logout.LogoutRequest;
import com.onelogin.saml2.logout.LogoutResponse;
import com.onelogin.saml2.servlet.ServletUtils;
import com.onelogin.saml2.settings.Saml2Settings;
import com.onelogin.saml2.settings.SettingsBuilder;
import com.onelogin.saml2.util.Constants;
import com.onelogin.saml2.util.Util;
/**
* Main class of OneLogin's Java Toolkit.
*
* This class implements the SP SAML instance.
* Defines the methods that you can invoke in your application in
* order to add SAML support (initiates sso, initiates slo, processes a
* SAML Response, a Logout Request or a Logout Response).
*
* This is stateful and not thread-safe, you should create a new instance for each request/response.
*/
public class Auth {
/**
* Private property to construct a logger for this class.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(Auth.class);
/**
* Settings data.
*/
private Saml2Settings settings;
/**
* HttpServletRequest object to be processed (Contains GET and POST parameters, session, ...).
*/
private HttpServletRequest request;
/**
* HttpServletResponse object to be used (For example to execute the redirections).
*/
private HttpServletResponse response;
/**
* NameID.
*/
private String nameid;
/**
* NameIDFormat.
*/
private String nameidFormat;
/**
* SessionIndex. When the user is logged, this stored it from the AuthnStatement of the SAML Response
*/
private String sessionIndex;
/**
* SessionNotOnOrAfter. When the user is logged, this stored it from the AuthnStatement of the SAML Response
*/
private DateTime sessionExpiration;
/**
* The ID of the last message processed
*/
private String lastMessageId;
/**
* The ID of the last assertion processed
*/
private String lastAssertionId;
/**
* The NotOnOrAfter values of the last assertion processed
*/
private List<Instant> lastAssertionNotOnOrAfter;
/**
* User attributes data.
*/
private Map<String, List<String>> attributes = new HashMap<String, List<String>>();
/**
* If user is authenticated.
*/
private boolean authenticated = false;
/**
* Stores any error.
*/
private List<String> errors = new ArrayList<String>();
/**
* Reason of the last error.
*/
private String errorReason;
/**
* The id of the last request (Authn or Logout) generated
*/
private String lastRequestId;
/**
* The most recently-constructed/processed XML SAML request
* (AuthNRequest, LogoutRequest)
*/
private String lastRequest;
/**
* The most recently-constructed/processed XML SAML response
* (SAMLResponse, LogoutResponse). If the SAMLResponse was
* encrypted, by default tries to return the decrypted XML
*/
private String lastResponse;
/**
* Initializes the SP SAML instance.
*
* @throws IOException
* @throws SettingsException
* @throws Error
*/
public Auth() throws IOException, SettingsException, Error {
this(new SettingsBuilder().fromFile("onelogin.saml.properties").build(), null, null);
}
/**
* Initializes the SP SAML instance.
*
* @param filename
* String Filename with the settings
*
* @throws IOException
* @throws SettingsException
* @throws Error
*/
public Auth(String filename) throws IOException, SettingsException, Error {
this(new SettingsBuilder().fromFile(filename).build(), null, null);
}
/**
* Initializes the SP SAML instance.
*
* @param request
* HttpServletRequest object to be processed
* @param response
* HttpServletResponse object to be used
*
* @throws IOException
* @throws SettingsException
* @throws Error
*/
public Auth(HttpServletRequest request, HttpServletResponse response) throws IOException, SettingsException, Error {
this(new SettingsBuilder().fromFile("onelogin.saml.properties").build(), request, response);
}
/**
* Initializes the SP SAML instance.
*
* @param filename
* String Filename with the settings
* @param request
* HttpServletRequest object to be processed
* @param response
* HttpServletResponse object to be used
*
* @throws SettingsException
* @throws IOException
* @throws Error
*/
public Auth(String filename, HttpServletRequest request, HttpServletResponse response) throws SettingsException, IOException, Error {
this(new SettingsBuilder().fromFile(filename).build(), request, response);
}
/**
* Initializes the SP SAML instance.
*
* @param settings
* Saml2Settings object. Setting data
* @param request
* HttpServletRequest object to be processed
* @param response
* HttpServletResponse object to be used
*
* @throws SettingsException
*/
public Auth(Saml2Settings settings, HttpServletRequest request, HttpServletResponse response) throws SettingsException {
this.settings = settings;
this.request = request;
this.response = response;
// Check settings
List<String> settingsErrors = settings.checkSettings();
if (!settingsErrors.isEmpty()) {
String errorMsg = "Invalid settings: ";
errorMsg += StringUtils.join(settingsErrors, ", ");
LOGGER.error(errorMsg);
throw new SettingsException(errorMsg, SettingsException.SETTINGS_INVALID);
}
LOGGER.debug("Settings validated");
}
/**
* Set the strict mode active/disable
*
* @param value
* Strict value
*/
public void setStrict(Boolean value)
{
settings.setStrict(value);
}
/**
* Initiates the SSO process.
*
* @param returnTo
* The target URL the user should be returned to after login (relayState).
* Will be a self-routed URL when null, or not be appended at all when an empty string is provided
* @param forceAuthn
* When true the AuthNRequest will set the ForceAuthn='true'
* @param isPassive
* When true the AuthNRequest will set the IsPassive='true'
* @param setNameIdPolicy
* When true the AuthNRequest will set a nameIdPolicy
* @param stay
* True if we want to stay (returns the url string) False to execute redirection
*
* @return the SSO URL with the AuthNRequest if stay = True
*
* @throws IOException
* @throws SettingsException
*/
public String login(String returnTo, Boolean forceAuthn, Boolean isPassive, Boolean setNameIdPolicy, Boolean stay) throws IOException, SettingsException {
Map<String, String> parameters = new HashMap<String, String>();
AuthnRequest authnRequest = new AuthnRequest(settings, forceAuthn, isPassive, setNameIdPolicy);
String samlRequest = authnRequest.getEncodedAuthnRequest();
parameters.put("SAMLRequest", samlRequest);
String relayState;
if (returnTo == null) {
relayState = ServletUtils.getSelfRoutedURLNoQuery(request);
} else {
relayState = returnTo;
}
if (!relayState.isEmpty()) {
parameters.put("RelayState", relayState);
}
if (settings.getAuthnRequestsSigned()) {
String sigAlg = settings.getSignatureAlgorithm();
String signature = this.buildRequestSignature(samlRequest, relayState, sigAlg);
parameters.put("SigAlg", sigAlg);
parameters.put("Signature", signature);
}
String ssoUrl = getSSOurl();
lastRequestId = authnRequest.getId();
lastRequest = authnRequest.getAuthnRequestXml();
if (!stay) {
LOGGER.debug("AuthNRequest sent to " + ssoUrl + " --> " + samlRequest);
}
return ServletUtils.sendRedirect(response, ssoUrl, parameters, stay);
}
/**
* Initiates the SSO process.
*
* @param returnTo
* The target URL the user should be returned to after login (relayState).
* Will be a self-routed URL when null, or not be appended at all when an empty string is provided
* @param forceAuthn
* When true the AuthNRequest will set the ForceAuthn='true'
* @param isPassive
* When true the AuthNRequest will set the IsPassive='true'
* @param setNameIdPolicy
* When true the AuthNRequest will set a nameIdPolicy
*
* @throws IOException
* @throws SettingsException
*/
public void login(String returnTo, Boolean forceAuthn, Boolean isPassive, Boolean setNameIdPolicy) throws IOException, SettingsException {
login(returnTo ,forceAuthn, isPassive, setNameIdPolicy, false);
}
/**
* Initiates the SSO process.
*
* @throws IOException
* @throws SettingsException
*/
public void login() throws IOException, SettingsException {
login(null ,false, false, true);
}
/**
* Initiates the SSO process.
*
* @param returnTo
* The target URL the user should be returned to after login (relayState).
* Will be a self-routed URL when null, or not be appended at all when an empty string is provided.
*
* @throws IOException
* @throws SettingsException
*/
public void login(String returnTo) throws IOException, SettingsException {
login(returnTo ,false, false, true);
}
/**
* Initiates the SLO process.
*
* @param returnTo
* The target URL the user should be returned to after logout (relayState).
* Will be a self-routed URL when null, or not be appended at all when an empty string is provided
* @param nameId
* The NameID that will be set in the LogoutRequest.
* @param sessionIndex
* The SessionIndex (taken from the SAML Response in the SSO process).
* @param stay
* True if we want to stay (returns the url string) False to execute redirection
* @param nameidFormat
* The NameID Format will be set in the LogoutRequest.
*
* @return the SLO URL with the LogoutRequest if stay = True
*
* @throws IOException
* @throws XMLEntityException
* @throws SettingsException
*/
public String logout(String returnTo, String nameId, String sessionIndex, Boolean stay, String nameidFormat) throws IOException, XMLEntityException, SettingsException {
Map<String, String> parameters = new HashMap<String, String>();
LogoutRequest logoutRequest = new LogoutRequest(settings, null, nameId, sessionIndex, nameidFormat);
String samlLogoutRequest = logoutRequest.getEncodedLogoutRequest();
parameters.put("SAMLRequest", samlLogoutRequest);
String relayState;
if (returnTo == null) {
relayState = ServletUtils.getSelfRoutedURLNoQuery(request);
} else {
relayState = returnTo;
}
if (!relayState.isEmpty()) {
parameters.put("RelayState", relayState);
}
if (settings.getLogoutRequestSigned()) {
String sigAlg = settings.getSignatureAlgorithm();
String signature = this.buildRequestSignature(samlLogoutRequest, relayState, sigAlg);
parameters.put("SigAlg", sigAlg);
parameters.put("Signature", signature);
}
String sloUrl = getSLOurl();
lastRequestId = logoutRequest.getId();
lastRequest = logoutRequest.getLogoutRequestXml();
if (!stay) {
LOGGER.debug("Logout request sent to " + sloUrl + " --> " + samlLogoutRequest);
}
return ServletUtils.sendRedirect(response, sloUrl, parameters, stay);
}
/**
* Initiates the SLO process.
*
* @param returnTo
* The target URL the user should be returned to after logout (relayState).
* Will be a self-routed URL when null, or not be appended at all when an empty string is provided
* @param nameId
* The NameID that will be set in the LogoutRequest.
* @param sessionIndex
* The SessionIndex (taken from the SAML Response in the SSO process).
* @param stay
* True if we want to stay (returns the url string) False to execute redirection
*
* @return the SLO URL with the LogoutRequest if stay = True
*
* @throws IOException
* @throws XMLEntityException
* @throws SettingsException
*/
public String logout(String returnTo, String nameId, String sessionIndex, Boolean stay) throws IOException, XMLEntityException, SettingsException {
return logout(returnTo, nameId, sessionIndex, stay, null);
}
/**
* Initiates the SLO process.
*
* @param returnTo
* The target URL the user should be returned to after logout (relayState).
* Will be a self-routed URL when null, or not be appended at all when an empty string is provided
* @param nameId
* The NameID that will be set in the LogoutRequest.
* @param sessionIndex
* The SessionIndex (taken from the SAML Response in the SSO process).
* @param nameidFormat
* The NameID Format will be set in the LogoutRequest.
* @throws IOException
* @throws XMLEntityException
* @throws SettingsException
*/
public void logout(String returnTo, String nameId, String sessionIndex, String nameidFormat) throws IOException, XMLEntityException, SettingsException {
logout(returnTo, nameId, sessionIndex, false, nameidFormat);
}
/**
* Initiates the SLO process.
*
* @param returnTo
* The target URL the user should be returned to after logout (relayState).
* Will be a self-routed URL when null, or not be appended at all when an empty string is provided
* @param nameId
* The NameID that will be set in the LogoutRequest.
* @param sessionIndex
* The SessionIndex (taken from the SAML Response in the SSO process).
*
* @throws IOException
* @throws XMLEntityException
* @throws SettingsException
*/
public void logout(String returnTo, String nameId, String sessionIndex) throws IOException, XMLEntityException, SettingsException {
logout(returnTo, nameId, sessionIndex, false, null);
}
/**
* Initiates the SLO process.
*
* @throws IOException
* @throws XMLEntityException
* @throws SettingsException
*/
public void logout() throws IOException, XMLEntityException, SettingsException {
logout(null, null, null, false);
}
/**
* Initiates the SLO process.
*
* @param returnTo
* The target URL the user should be returned to after logout (relayState).
* Will be a self-routed URL when null, or not be appended at all when an empty string is provided
*
* @throws IOException
* @throws XMLEntityException
* @throws SettingsException
*/
public void logout(String returnTo) throws IOException, XMLEntityException, SettingsException {
logout(returnTo, null, null);
}
/**
* @return The url of the Single Sign On Service
*/
public String getSSOurl() {
return settings.getIdpSingleSignOnServiceUrl().toString();
}
/**
* @return The url of the Single Logout Service
*/
public String getSLOurl() {
return settings.getIdpSingleLogoutServiceUrl().toString();
}
/**
* @return The url of the Single Logout Service Response.
*/
public String getSLOResponseUrl() {
return settings.getIdpSingleLogoutServiceResponseUrl().toString();
}
/**
* Process the SAML Response sent by the IdP.
*
* @param requestId
* The ID of the AuthNRequest sent by this SP to the IdP
*
* @throws Exception
*/
public void processResponse(String requestId) throws Exception {
authenticated = false;
final HttpRequest httpRequest = ServletUtils.makeHttpRequest(this.request);
final String samlResponseParameter = httpRequest.getParameter("SAMLResponse");
if (samlResponseParameter != null) {
SamlResponse samlResponse = new SamlResponse(settings, httpRequest);
lastResponse = samlResponse.getSAMLResponseXml();
if (samlResponse.isValid(requestId)) {
nameid = samlResponse.getNameId();
nameidFormat = samlResponse.getNameIdFormat();
authenticated = true;
attributes = samlResponse.getAttributes();
sessionIndex = samlResponse.getSessionIndex();
sessionExpiration = samlResponse.getSessionNotOnOrAfter();
lastMessageId = samlResponse.getId();
lastAssertionId = samlResponse.getAssertionId();
lastAssertionNotOnOrAfter = samlResponse.getAssertionNotOnOrAfter();
LOGGER.debug("processResponse success --> " + samlResponseParameter);
} else {
errors.add("invalid_response");
LOGGER.error("processResponse error. invalid_response");
LOGGER.debug(" --> " + samlResponseParameter);
errorReason = samlResponse.getError();
}
} else {
errors.add("invalid_binding");
String errorMsg = "SAML Response not found, Only supported HTTP_POST Binding";
LOGGER.error("processResponse error." + errorMsg);
throw new Error(errorMsg, Error.SAML_RESPONSE_NOT_FOUND);
}
}
/**
* Process the SAML Response sent by the IdP.
*
* @throws Exception
*/
public void processResponse() throws Exception {
processResponse(null);
}
/**
* Process the SAML Logout Response / Logout Request sent by the IdP.
*
* @param keepLocalSession
* When false will destroy the local session, otherwise will destroy it
* @param requestId
* The ID of the LogoutRequest sent by this SP to the IdP
*
* @throws Exception
*/
public void processSLO(Boolean keepLocalSession, String requestId) throws Exception {
final HttpRequest httpRequest = ServletUtils.makeHttpRequest(this.request);
final String samlRequestParameter = httpRequest.getParameter("SAMLRequest");
final String samlResponseParameter = httpRequest.getParameter("SAMLResponse");
if (samlResponseParameter != null) {
LogoutResponse logoutResponse = new LogoutResponse(settings, httpRequest);
lastResponse = logoutResponse.getLogoutResponseXml();
if (!logoutResponse.isValid(requestId)) {
errors.add("invalid_logout_response");
LOGGER.error("processSLO error. invalid_logout_response");
LOGGER.debug(" --> " + samlResponseParameter);
errorReason = logoutResponse.getError();
} else {
String status = logoutResponse.getStatus();
if (status == null || !status.equals(Constants.STATUS_SUCCESS)) {
errors.add("logout_not_success");
LOGGER.error("processSLO error. logout_not_success");
LOGGER.debug(" --> " + samlResponseParameter);
} else {
lastMessageId = logoutResponse.getId();
LOGGER.debug("processSLO success --> " + samlResponseParameter);
if (!keepLocalSession) {
request.getSession().invalidate();
}
}
}
} else if (samlRequestParameter != null) {
LogoutRequest logoutRequest = new LogoutRequest(settings, httpRequest);
lastRequest = logoutRequest.getLogoutRequestXml();
if (!logoutRequest.isValid()) {
errors.add("invalid_logout_request");
LOGGER.error("processSLO error. invalid_logout_request");
LOGGER.debug(" --> " + samlRequestParameter);
errorReason = logoutRequest.getError();
} else {
lastMessageId = logoutRequest.getId();
LOGGER.debug("processSLO success --> " + samlRequestParameter);
if (!keepLocalSession) {
request.getSession().invalidate();
}
String inResponseTo = logoutRequest.id;
LogoutResponse logoutResponseBuilder = new LogoutResponse(settings, httpRequest);
logoutResponseBuilder.build(inResponseTo);
lastResponse = logoutResponseBuilder.getLogoutResponseXml();
String samlLogoutResponse = logoutResponseBuilder.getEncodedLogoutResponse();
Map<String, String> parameters = new LinkedHashMap<String, String>();
parameters.put("SAMLResponse", samlLogoutResponse);
String relayState = request.getParameter("RelayState");
if (relayState != null) {
parameters.put("RelayState", relayState);
}
if (settings.getLogoutRequestSigned()) {
String sigAlg = settings.getSignatureAlgorithm();
String signature = this.buildResponseSignature(samlLogoutResponse, relayState, sigAlg);
parameters.put("SigAlg", sigAlg);
parameters.put("Signature", signature);
}
String sloUrl = getSLOResponseUrl();
LOGGER.debug("Logout response sent to " + sloUrl + " --> " + samlLogoutResponse);
ServletUtils.sendRedirect(response, sloUrl, parameters);
}
} else {
errors.add("invalid_binding");
String errorMsg = "SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding";
LOGGER.error("processSLO error." + errorMsg);
throw new Error(errorMsg, Error.SAML_LOGOUTMESSAGE_NOT_FOUND);
}
}
/**
* Process the SAML Logout Response / Logout Request sent by the IdP.
*
* @throws Exception
*/
public void processSLO() throws Exception {
processSLO(false, null);
}
/**
* @return the authenticated
*/
public final boolean isAuthenticated() {
return authenticated;
}
/**
* @return the list of the names of the SAML attributes.
*/
public final List<String> getAttributesName() {
return new ArrayList<String>(attributes.keySet());
}
/**
* @return the set of SAML attributes.
*/
public final Map<String, List<String>> getAttributes() {
return attributes;
}
/**
* @param name
* Name of the attribute
*
* @return the attribute value
*/
public final Collection<String> getAttribute(String name) {
return attributes.get(name);
}
/**
* @return the nameID of the assertion
*/
public final String getNameId()
{
return nameid;
}
/**
* @return the nameID Format of the assertion
*/
public final String getNameIdFormat()
{
return nameidFormat;
}
/**
* @return the SessionIndex of the assertion
*/
public final String getSessionIndex()
{
return sessionIndex;
}
/**
* @return the SessionNotOnOrAfter of the assertion
*/
public final DateTime getSessionExpiration()
{
return sessionExpiration;
}
/**
* @return The ID of the last message processed
*/
public String getLastMessageId() {
return lastMessageId;
}
/**
* @return The ID of the last assertion processed
*/
public String getLastAssertionId() {
return lastAssertionId;
}
/**
* @return The NotOnOrAfter values of the last assertion processed
*/
public List<Instant> getLastAssertionNotOnOrAfter() {
return lastAssertionNotOnOrAfter;
}
/**
* @return an array with the errors, the array is empty when the validation was successful
*/
public List<String> getErrors()
{
return errors;
}
/**
* @return the reason for the last error
*/
public String getLastErrorReason()
{
return errorReason;
}
/**
* @return the id of the last request generated (AuthnRequest or LogoutRequest), null if none
*/
public String getLastRequestId()
{
return lastRequestId;
}
/**
* @return the Saml2Settings object. The Settings data.
*/
public Saml2Settings getSettings()
{
return settings;
}
/**
* @return if debug mode is active
*/
public Boolean isDebugActive() {
return settings.isDebugActive();
}
/**
* Generates the Signature for a SAML Request
*
* @param samlRequest
* The SAML Request
* @param relayState
* The RelayState
* @param signAlgorithm
* Signature algorithm method
*
* @return a base64 encoded signature
*
* @throws SettingsException
*/
public String buildRequestSignature(String samlRequest, String relayState, String signAlgorithm) throws SettingsException
{
return buildSignature(samlRequest, relayState, signAlgorithm, "SAMLRequest");
}
/**
* Generates the Signature for a SAML Response
*
* @param samlResponse
* The SAML Response
* @param relayState
* The RelayState
* @param signAlgorithm
* Signature algorithm method
*
* @return the base64 encoded signature
*
* @throws SettingsException
*/
public String buildResponseSignature(String samlResponse, String relayState, String signAlgorithm) throws SettingsException
{
return buildSignature(samlResponse, relayState, signAlgorithm, "SAMLResponse");
}
/**
* Generates the Signature for a SAML Response
*
* @param samlResponse
* The SAML Response
* @param relayState
* The RelayState
* @param signAlgorithm
* Signature algorithm method
* @param type
* The type of the message
*
* @return the base64 encoded signature
*
* @throws SettingsException
* @throws IllegalArgumentException
*/
private String buildSignature(String samlMessage, String relayState, String signAlgorithm, String type) throws SettingsException, IllegalArgumentException
{
String signature = "";
if (!settings.checkSPCerts()) {
String errorMsg = "Trying to sign the " + type + " but can't load the SP private key";
LOGGER.error("buildSignature error. " + errorMsg);
throw new SettingsException(errorMsg, SettingsException.PRIVATE_KEY_NOT_FOUND);
}
PrivateKey key = settings.getSPkey();
String msg = type + "=" + Util.urlEncoder(samlMessage);
if (StringUtils.isNotEmpty(relayState)) {
msg += "&RelayState=" + Util.urlEncoder(relayState);
}
if (StringUtils.isEmpty(signAlgorithm)) {
signAlgorithm = Constants.RSA_SHA1;
}
msg += "&SigAlg=" + Util.urlEncoder(signAlgorithm);
try {
signature = Util.base64encoder(Util.sign(msg, key, signAlgorithm));
} catch (InvalidKeyException | NoSuchAlgorithmException | SignatureException e) {
String errorMsg = "buildSignature error." + e.getMessage();
LOGGER.error(errorMsg);
}
if (signature.isEmpty()) {
String errorMsg = "There was a problem when calculating the Signature of the " + type;
LOGGER.error("buildSignature error. " + errorMsg);
throw new IllegalArgumentException(errorMsg);
}
LOGGER.debug("buildResponseSignature success. --> " + signature);
return signature;
}
/**
* Returns the most recently-constructed/processed
* XML SAML request (AuthNRequest, LogoutRequest)
*
* @return the last Request XML
*/
public String getLastRequestXML()
{
return lastRequest;
}
/**
* Returns the most recently-constructed/processed
* XML SAML response (SAMLResponse, LogoutResponse).
* If the SAMLResponse was encrypted, by default tries
* to return the decrypted XML.
*
* @return the last Response XML
*/
public String getLastResponseXML()
{
return lastResponse;
}
}