Get started
This page takes you from an API key to a signed hash: install a client, create a session, send the user to a provider, and use the session that comes back. Each step has code in C#, PHP, Python, Node.js and Java. How it works explains the flow behind the steps.
The code signs a hash that your signature library computed. Signing a PDF and Authentication show complete handlers that compute that hash and finish the signature with PKI SDK, Rest PKI or PKI Express.
1. Get an API key
Ask Lacuna Software support (suporte@lacunasoftware.com) for a CloudHub API key, and list the
redirect URIs your application will use, for production and for development. If you will add your
own parameters to the redirect URI, as the examples below do with fileId, ask for a host entry
such as https://app.example.com instead of an exact URI. See
Redirect URIs.
The examples read the key from the CLOUDHUB_API_KEY environment variable. Keep it on the server.
2. Install a client
- C#
- PHP
- Python
- Node.js
- Java
dotnet add package Lacuna.Cloudhub.Client
For ASP.NET Core dependency injection, add Lacuna.Cloudhub.ClientCore as well.
composer require lacuna/cloudhub-client
The client needs PHP 7.4 or later and uses Guzzle.
pip install requests
The Python examples call the REST API directly; there are only eight operations.
npm install cloudhub-client
dependencies {
implementation 'com.lacunasoftware.cloudhub:cloudhub-client:1.0.5'
}
With Maven:
<dependency>
<groupId>com.lacunasoftware.cloudhub</groupId>
<artifactId>cloudhub-client</artifactId>
<version>1.0.5</version>
</dependency>
3. Create a client
- C#
- PHP
- Python
- Node.js
- Java
using Lacuna.Cloudhub.Api;
using Lacuna.Cloudhub.Client;
var cloudhub = new CloudhubClient(
"https://cloudhub.lacunasoftware.com/",
Environment.GetEnvironmentVariable("CLOUDHUB_API_KEY"));
With Lacuna.Cloudhub.ClientCore, register the client once and inject ICloudhubService, which has
the same methods:
using Lacuna.Cloudhub.ClientCore;
builder.Services.AddCloudhub().Configure(builder.Configuration.GetSection("Cloudhub"));
{
"Cloudhub": {
"Endpoint": "https://cloudhub.lacunasoftware.com/",
"ApiKey": "set it in user secrets or an environment variable"
}
}
<?php
require __DIR__ . '/vendor/autoload.php';
use Lacuna\Cloudhub\CloudhubClient;
// The client appends "api/..." to the address, so keep the trailing slash
$cloudhub = new CloudhubClient('https://cloudhub.lacunasoftware.com/', getenv('CLOUDHUB_API_KEY'));
The PHP methods end in Async but are synchronous: each one returns the result.
import os
import requests
CLOUDHUB_URL = 'https://cloudhub.lacunasoftware.com'
_http = requests.Session()
_http.headers['X-Api-Key'] = os.environ['CLOUDHUB_API_KEY']
class CloudHubError(Exception):
"""An error CloudHub reported, such as SessionExpired."""
def __init__(self, code, message):
super().__init__(f'{code}: {message}')
self.code = code
def call_cloudhub(method, path, **kwargs):
# A signature can wait on the provider, so allow a generous timeout
response = _http.request(method, CLOUDHUB_URL + path, timeout=120, **kwargs)
if response.status_code == 422:
error = response.json()
raise CloudHubError(error.get('code'), error.get('message'))
response.raise_for_status()
return response.json()
const { CloudHubClient, TrustServiceSessionTypes } = require('cloudhub-client');
const cloudhub = new CloudHubClient('https://cloudhub.lacunasoftware.com/', process.env.CLOUDHUB_API_KEY);
import cloudhub.SessionsApi;
import cloudhub.client.CloudhubClient;
// No trailing slash: the client appends "/api/..." to the address
SessionsApi cloudhub = new SessionsApi(
new CloudhubClient("https://cloudhub.lacunasoftware.com", System.getenv("CLOUDHUB_API_KEY")));
A CloudhubClient keeps per-request state, so don't share one instance between threads that call
it at the same time.
4. Create a session
Send the signer's CPF, digits only, and the URI CloudHub should send the user back to. The examples carry the ID of the file to sign in the redirect URI, so that the request that comes back knows which file it is for.
- C#
- PHP
- Python
- Node.js
- Java
var session = await cloudhub.CreateSessionAsync(new SessionCreateRequest {
Identifier = "12345678909",
RedirectUri = "https://app.example.com/sign/complete?fileId=42",
Type = TrustServiceSessionTypes.SingleSignature,
LifetimeInSeconds = 300,
});
foreach (var service in session.Services) {
Console.WriteLine($"{service.ServiceInfo.Provider}: {service.AuthUrl}");
}
use Lacuna\Cloudhub\SessionCreateRequest;
use Lacuna\Cloudhub\TrustServiceSessionTypes;
$session = $cloudhub->createSessionAsync(new SessionCreateRequest(
'12345678909', // the signer's CPF
'https://app.example.com/sign/complete?fileId=42', // redirect URI
TrustServiceSessionTypes::SingleSignature,
300 // lifetime in seconds; 300 if omitted
));
foreach ($session->services as $service) {
echo $service->serviceInfo->provider . ': ' . $service->authUrl . PHP_EOL;
}
from cloudhub import call_cloudhub
session = call_cloudhub('POST', '/api/sessions', json={
'identifier': '12345678909',
'redirectUri': 'https://app.example.com/sign/complete?fileId=42',
'type': 'SingleSignature',
'lifetimeInSeconds': 300,
})
for service in session['services']:
print(service['serviceInfo']['provider'], service['authUrl'])
const session = await cloudhub.createSessionAsync({
identifier: '12345678909',
redirectUri: 'https://app.example.com/sign/complete?fileId=42',
type: TrustServiceSessionTypes.SingleSignature,
lifetimeInSeconds: 300,
});
for (const service of session.services || []) {
console.log(`${service.serviceInfo.provider}: ${service.authUrl}`);
}
import cloudhub.client.model.SessionCreateRequest;
import cloudhub.client.model.SessionModel;
import cloudhub.client.model.TrustServiceAuthParametersModel;
import cloudhub.client.model.TrustServiceSessionTypes;
SessionCreateRequest request = new SessionCreateRequest();
request.setIdentifier("12345678909");
request.setRedirectUri("https://app.example.com/sign/complete?fileId=42");
request.setType(TrustServiceSessionTypes.SINGLE_SIGNATURE);
request.setLifetimeInSeconds(300);
SessionModel session = cloudhub.apiSessionsPost(request);
for (TrustServiceAuthParametersModel service : session.getServices()) {
System.out.println(service.getServiceInfo().getProvider() + ": " + service.getAuthUrl());
}
An empty list means that no provider holds a certificate for that CPF. Tell the user, and offer to try another CPF.
5. Send the user to a provider
Render one choice per entry and navigate the browser to the authUrl the user picks. badgeUrl
is the provider's logo and may be null.
<!-- One link per entry of "services" -->
<a href="https://api.birdid.com.br/oauth/authorize?client_id=…">
<img src="https://cdn.lacunasoftware.com/img/psc/birdid.png" alt="BirdID" height="48">
Sign with SOLUTI
</a>
The user signs in at the provider and approves. The provider sends the browser back to CloudHub, and CloudHub sends it to your redirect URI.
6. Read the certificate when the user comes back
Your redirect URI receives the session parameter beside your own. When session is missing, the
user did not authorize: send them back to choose again.
- C#
- PHP
- Python
- Node.js
- Java
// ASP.NET Core: GET /sign/complete?fileId=42&session=…
public async Task<IActionResult> Complete(string fileId, string session) {
if (string.IsNullOrEmpty(session)) {
return RedirectToAction("Index", new { fileId });
}
byte[] certificate = await cloudhub.GetCertificateAsync(session);
// ...
}
// complete.php?fileId=42&session=…
$fileId = $_GET['fileId'];
$session = $_GET['session'] ?? null;
if (empty($session)) {
header('Location: /sign.php?fileId=' . urlencode($fileId));
exit;
}
$certificate = $cloudhub->getCertificateAsync($session); // base64
from flask import redirect, request, url_for
@app.route('/sign/complete')
def complete():
file_id = request.args['fileId']
session = request.args.get('session')
if not session:
return redirect(url_for('sign', fileId=file_id))
certificate = call_cloudhub('GET', '/api/sessions/certificate',
params={'session': session}) # base64
# ...
router.get('/sign/complete', async (req, res, next) => {
try {
const { fileId, session } = req.query;
if (!session) {
return res.redirect(`/sign?fileId=${encodeURIComponent(fileId)}`);
}
const certificate = await cloudhub.getCertificateAsync(session); // base64
// ...
} catch (err) {
next(err);
}
});
import cloudhub.client.ApiException;
import java.nio.charset.StandardCharsets;
@GetMapping("/sign/complete")
public String complete(@RequestParam String fileId,
@RequestParam(required = false) String session) throws ApiException {
if (session == null || session.isEmpty()) {
return "redirect:/sign?fileId=" + fileId;
}
String certificate = unquote(cloudhub.apiSessionsCertificateGet(session)); // base64
// ...
}
// The certificate and sign-hash operations answer with a JSON string ("MIIG..."),
// and the client hands back its raw bytes, quotes included
private static String unquote(byte[] body) {
return new String(body, StandardCharsets.UTF_8).replace("\"", "");
}
Before you sign, check that the certificate belongs to the person you expected: compare the CPF in it with the one you sent. The complete examples do this with their signature library.
7. Sign a hash
Your signature library gives you the hash to sign and its digest algorithm. Send both with the
session; the result is the signature value, for your library to embed in the document. Identify the
algorithm by OID (digestAlgorithmOid), as the signature libraries report it, or by name
(digestAlgorithm, such as SHA-256).
- C#
- PHP
- Python
- Node.js
- Java
byte[] signature = await cloudhub.SignHashAsync(new SignHashRequest {
Session = session,
Hash = toSignHash, // the hash, as bytes
DigestAlgorithmOid = digestAlgorithmOid,
});
use Lacuna\Cloudhub\SignHashRequest;
$signature = $cloudhub->signHashAsync(new SignHashRequest(
$session,
$toSignHashBase64, // the hash, base64-encoded
null, // the digest algorithm name...
$digestAlgorithmOid // ...or its OID
)); // base64
signature = call_cloudhub('POST', '/api/sessions/sign-hash', json={
'session': session,
'hash': to_sign_hash_base64,
'digestAlgorithmOid': digest_algorithm_oid,
}) # base64
const signature = await cloudhub.signHashAsync({
session,
hash: toSignHashBase64,
digestAlgorithmOid,
}); // base64
import cloudhub.client.model.SignHashRequest;
import java.util.Base64;
SignHashRequest signRequest = new SignHashRequest();
signRequest.setSession(session);
signRequest.setHash(toSignHash); // the hash, as bytes
signRequest.setDigestAlgorithmOid(digestAlgorithmOid);
byte[] signature = Base64.getDecoder().decode(unquote(cloudhub.apiSessionsSignHashPost(signRequest)));
8. Handle errors
CloudHub reports a problem with your request as HTTP 422 and a JSON body with a code, a message
and sometimes details. The code you will meet most is SessionExpired: the session ran out
before you used it, so send the user through the provider again. The
REST API page lists every code.
- C#
- PHP
- Python
- Node.js
- Java
try {
var signature = await cloudhub.SignHashAsync(request);
} catch (CloudhubApiException ex) when (ex.CodeStr == "SessionExpired") {
// Create a new session and send the user to the provider again
}
Compare CodeStr. The Code enum property is not filled in from the response.
use GuzzleHttp\Exception\ClientException;
try {
$signature = $cloudhub->signHashAsync($request);
} catch (ClientException $e) {
$error = json_decode((string) $e->getResponse()->getBody(), true);
if (($error['code'] ?? null) === 'SessionExpired') {
// Create a new session and send the user to the provider again
}
}
from cloudhub import CloudHubError
try:
signature = call_cloudhub('POST', '/api/sessions/sign-hash', json=sign_request)
except CloudHubError as e:
if e.code == 'SessionExpired':
pass # Create a new session and send the user to the provider again
try {
const signature = await cloudhub.signHashAsync(request);
} catch (err) {
// cloudhub-client 1.x throws a string: CloudHub's JSON error followed by the HTTP message
if (String(err).includes('"SessionExpired"')) {
// Create a new session and send the user to the provider again
}
}
try {
byte[] signature = cloudhub.apiSessionsSignHashPost(request);
} catch (ApiException e) {
if (e.getCode() == 422 && e.getResponseBody().contains("\"SessionExpired\"")) {
// Create a new session and send the user to the provider again
}
}
A request without a valid API key gets HTTP 401. An unexpected failure, such as a provider that is down, gets HTTP 500 with a short code in the body; quote it when you contact support.
Next steps
- Signing a PDF: the complete PAdES flow, with the signature library computing the hash and finishing the PDF.
- Authentication: sign users in with their cloud certificate.
- How it works:
SignatureSessionfor signing several documents with one authorization.