package cloud; import java.io.IOException; /** * Represents an error returned by ACS. * */ public class ACSException extends IOException { private static final long serialVersionUID = 6210440524876587564L; private final int statusCode; public ACSException(String message, int statusCode) { super(message); this.statusCode = statusCode; } /** * Gets the HTTP status code returned by ACS. * @return HTTP status code */ public int getStatusCode() { return statusCode; } } --- package cloud; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.AuthCache; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.client.utils.URIBuilder; import org.apache.http.impl.auth.BasicScheme; import org.apache.http.impl.client.BasicAuthCache; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; /** * CloudTicket is an abstraction for ACS sessions, and provides code to obtain, * cancel, and retrieve the underlying SAML assertion for ACS sessions. * * The Apache HTTP client is used, but this code could be switched over to use * any HTTP implementation that supports TLS 1.2. * */ public final class CloudTicket { public static final String DEFAULT_ACS_SERVER = "acs.laserfiche.com"; private String sessionKey_; private URI endpoint_; private CloseableHttpClient httpclient; private CloudTicket(String sessionKey, URI endpoint, CloseableHttpClient httpclient) { if (sessionKey == null) throw new IllegalArgumentException("sessionKey"); if (endpoint == null) throw new IllegalArgumentException("endpoint"); sessionKey_ = sessionKey; endpoint_ = endpoint; this.httpclient = httpclient; } /** * Gets the ACS session key * * @return An ACS session key */ public String getSessionKey() { return sessionKey_; } /** * Gets the base ACS endpoint URL * * @return A URI representing the base ACS endpoint */ public URI getEndpoint() { return endpoint_; } /** * Cancels the ACS session, and releases underlying resources. * * @throws IOException */ public void cancelSession() throws IOException { if (httpclient == null) return; HttpPost request = new HttpPost(endpoint_.toString() + "/cancelsession"); request.addHeader("Accept", "text/plain"); request.addHeader("X-Lf-Acs-SessionKey", sessionKey_); CloseableHttpResponse response = httpclient.execute(request); boolean succeeded = false; try { int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != 200 && statusCode != 204) throw new ACSException("POST /cancelsession returned an error.", statusCode); succeeded = true; } finally { response.close(); if (succeeded) { httpclient.close(); httpclient = null; } } } /** * Returns the underlying SAML assertion for the ACS session. * * @return A String that contains the raw SAML assertion * @throws IOException */ public String getSAMLAssertion() throws IOException { if (httpclient == null) throw new IOException("CloudTicket instance already closed."); HttpGet request = new HttpGet(endpoint_.toString() + "/getsecuritytoken"); request.addHeader("Accept", "text/plain"); request.addHeader("X-Lf-Acs-SessionKey", sessionKey_); CloseableHttpResponse response = httpclient.execute(request); try { int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != 200) throw new ACSException("GET /getsecuritytoken returned an error.", statusCode); HttpEntity entity = response.getEntity(); String assertion = EntityUtils.toString(entity); return assertion; } catch (UnsupportedEncodingException e) { throw new IOException("Response could not be decoded.", e); } finally { response.close(); } } /** * Authenticates with ACS and returns a CloudTicket encapsulating an ACS session * on success. * * @param settings Credentials and the ACS endpoint to use * @return A CloudTicket * @throws IOException * @throws URISyntaxException */ public static CloudTicket getTicket(CloudTicketRequestSettings settings) throws IOException, URISyntaxException { if (settings == null) throw new IllegalArgumentException("settings"); CloseableHttpClient httpclient = HttpClients.createDefault(); URI endpoint = settings.getCustomEndpoint(); if (endpoint == null) { endpoint = new URIBuilder().setScheme("https").setHost(DEFAULT_ACS_SERVER).setPath("/ACS").build(); } StringBuilder query = new StringBuilder(); query.append("customerID="); query.append(settings.getAccountId()); if (settings.getOneTimePassword() != null) { query.append("&mfaCode="); query.append(settings.getOneTimePassword()); } HttpHost httpHost = new HttpHost(endpoint.getHost(), endpoint.getScheme() == "http" ? 80 : 443, endpoint.getScheme()); CredentialsProvider credsProvider = new BasicCredentialsProvider(); credsProvider.setCredentials(new AuthScope(httpHost.getHostName(), httpHost.getPort()), new UsernamePasswordCredentials(settings.getUserName(), settings.getPassword())); AuthCache authCache = new BasicAuthCache(); BasicScheme basicAuth = new BasicScheme(); authCache.put(httpHost, basicAuth); HttpClientContext context = HttpClientContext.create(); context.setCredentialsProvider(credsProvider); context.setAuthCache(authCache); HttpPost loginRequest = new HttpPost(endpoint.getRawPath() + "/login?" + query.toString()); loginRequest.addHeader("Accept", "text/plain"); if (settings.getClientAddress() != null) loginRequest.addHeader("X-Forwarded-For", settings.getClientAddress()); CloseableHttpResponse response = httpclient.execute(httpHost, loginRequest, context); CloudTicket ticket = null; try { int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != 200) throw new ACSException("Failed to get session key from ACS.", statusCode); HttpEntity entity = response.getEntity(); String sessionKey = EntityUtils.toString(entity); ticket = new CloudTicket(sessionKey, endpoint, httpclient); } finally { response.close(); if (ticket == null) httpclient.close(); } return ticket; } } --- package cloud; import java.net.URI; /** * A collection of parameters and settings used to obtain a CloudTicket. * */ public class CloudTicketRequestSettings { private URI customEndpoint; private String accountId; private String userName; private String password; private String oneTimePassword; private String clientAddress; public URI getCustomEndpoint() { return customEndpoint; } public void setCustomEndpoint(URI customEndpoint) { this.customEndpoint = customEndpoint; } public String getAccountId() { return accountId; } public void setAccountId(String accountId) { this.accountId = accountId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getOneTimePassword() { return oneTimePassword; } public void setOneTimePassword(String oneTimePassword) { this.oneTimePassword = oneTimePassword; } public String getClientAddress() { return clientAddress; } public void setClientAddress(String clientAddress) { this.clientAddress = clientAddress; } } --- package cloud; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.DocumentHelper; import org.dom4j.Node; import org.dom4j.XPath; /** * Encapsulates information about SAML assertions. * * Currently bare-bones, but could be extended to parse more information from * SAML assertions issued by ACS. * */ public class SAMLAssertion { private List repositories_ = new ArrayList(); private final static Map namespaces = new HashMap(); static { namespaces.put("a", "urn:oasis:names:tc:SAML:2.0:assertion"); } /** * Parses a string containing an ACS SAML assertion. * * @param rawToken A String containing a SAML assertion XML document * @throws DocumentException */ public SAMLAssertion(String rawToken) throws DocumentException { Document document = DocumentHelper.parseText(rawToken); XPath xpath = document.createXPath( "//a:Attribute[@Name='http://laserfiche.com/identity/claims/catalyst/roles']/a:AttributeValue"); xpath.setNamespaceURIs(namespaces); List nodes = xpath.selectNodes(document); for (int i = 0; i < nodes.size(); i++) { Node attrValueNode = nodes.get(i); String role = attrValueNode.getText(); if (role.startsWith("r-")) { int separatorIdx = role.indexOf(':'); if (separatorIdx > 0) { String roleName = role.substring(separatorIdx + 1); if (roleName.equals("CreateSession")) { String repository = role.substring(0, separatorIdx); repositories_.add(repository); } } } } } /** * Gets an array of repository names for which the assertion indicates the * CreateSession role. * * @return an array of repository names */ public String[] getRepositoryNames() { String[] rlist = new String[repositories_.size()]; return repositories_.toArray(rlist); } } --- package cloud; import java.io.IOException; import java.net.URISyntaxException; import org.dom4j.DocumentException; import com.laserfiche.repositoryaccess.FieldCollection; import com.laserfiche.repositoryaccess.RepositoryRegistration; import com.laserfiche.repositoryaccess.Session; import com.laserfiche.repositoryaccess.Template; /** * Sample code to show off getting the list of repositories in Laserfiche Cloud * that a user is authorized to log in to, and signing in to a Laserfiche * repository. * * The sample code works for the U.S. region. For the Canada region, a different * ACS endpoint must be used, and repository host names end with "laserfiche.ca" * instead of "laserfiche.com". * */ public class SignInSample { /** * Print the fields that are assigned to the "Email" template. * * @param session A Laserfiche JRepositoryAccess Session object */ public static void printFields(Session session) { Template emailTemplate = Template.getByName("Email", session); FieldCollection fields = emailTemplate.getFields(); for (int i = 0; i < fields.size(); i++) { System.out.println(fields.get(i).getName()); } } public static void main(String[] args) { final String accountId = "123456789 CHANGE ME"; final String userName = "the-user-name CHANGE ME"; final String password = "the-password CHANGE ME"; // First, we instantiate CloudTicketRequestSettings and specify the // account ID, user name, and password for a valid Laserfiche Cloud // user account that is enabled and has an assigned user subscription seat. CloudTicketRequestSettings ctrs = new CloudTicketRequestSettings(); ctrs.setAccountId(accountId); ctrs.setUserName(userName); ctrs.setPassword(password); // Uncomment the following line if signing in to the Canada region: // ctrs.setCustomEndpoint(new URI("https://acs.laserfiche.ca/ACS")); try { // Now, obtain a CloudTicket, which is a wrapper around an ACS session token. CloudTicket ticket = CloudTicket.getTicket(ctrs); // The ACS session token can be used to retrieve the underlying SAML assertion. // The assertion contains claims that list the authorized repository IDs. String rawSAML = ticket.getSAMLAssertion(); // The SAMLAssertion class is used to parse the SAML assertion and extract // the repository names. SAMLAssertion saml = new SAMLAssertion(rawSAML); String[] repositoryNames = saml.getRepositoryNames(); if (repositoryNames.length == 0) { System.out.println("The user is not authorized to log in to any repositories."); ticket.cancelSession(); return; } // Append ".laserfiche.ca" for accounts in the Canada region instead of // "laserfiche.com". String repositoryHost = repositoryNames[0] + ".laserfiche.com"; // Signing in to LFS hosted in Laserfiche Cloud works very similarly to // on-prem. However, LFS in Cloud does not support getting a repository // list. Also, SSL/TLS MUST BE USED, or else things will not work properly. System.out.println("Signing in to " + repositoryHost); RepositoryRegistration rr = new RepositoryRegistration(repositoryHost, repositoryHost, 80, 443); Session session = new Session(); session.setSecure(true); // LFS in LF Cloud supports signing in with ACS session keys. This is slightly // more efficient than passing in a user name and password, although that works // as well. session.logIn(rr, "SESSION_KEY$", ticket.getSessionKey()); // To avoid using excessive resources and to minimize security risk, cancel // the ACS session as soon as a LFS session is created. Canceling ACS sessions // does not affect the status of any LFS sessions created from the ACS session // key. ticket.cancelSession(); printFields(session); session.logOut(); System.out.println("All done!"); } catch (IOException e) { System.err.println(e); } catch (URISyntaxException e) { System.err.println(e); } catch (DocumentException e) { System.err.println(e); } } }