---
title: "Authentication with a cloud certificate"
description: "Sign users in with their cloud certificate through CloudHub: the user signs a nonce at their provider and your signature library checks the signature and validates the certificate."
canonical: "https://docs.lacunasoftware.com/en-us/articles/cloudhub/authentication"
locale: "en"
product: "CloudHub"
last_updated: "2026-10-12"
---

# Authentication with a cloud certificate

A cloud certificate can sign users in to your application the same way a smart card does: the user
proves that they control the certificate's key by signing a random challenge (a **nonce**), and your
signature library checks the signature and validates the certificate. With CloudHub, the user
approves that signature in their provider's app instead of typing a PIN into a token.

The flow is the [signature flow](https://docs.lacunasoftware.com/en-us/articles/cloudhub/how-it-works#the-flow-step-by-step) with a nonce in place of a
document:

1. Create a session with `SingleSignature` and send the user to a provider.
2. When the user comes back, read their certificate from CloudHub.
3. Have your signature library create a nonce, and have CloudHub sign the nonce's hash.
4. Have your signature library check the signature and validate the certificate.
5. Find the user by the certificate's CPF and sign them in.

Use `SingleSignature` for this, not `AuthenticationSession`: most providers don't support
authentication sessions, and a session that cannot sign cannot prove anything about the key.

The certificate alone is not proof. Step 4 is what shows that the user controls the key right now and
that the certificate is valid and trusted.

## The example

Each example is the redirect URI's handler; the handler that creates the session is the same as in
[Get started](https://docs.lacunasoftware.com/en-us/articles/cloudhub/get-started#4-create-a-session), without an identifier if you prefer to let the user
identify themselves at the provider. The examples follow the samples in
[PKI Suite Samples](https://github.com/LacunaSoftware/PkiSuiteSamples):

| Language | Signature library |
|---|---|
| C# | PKI SDK (`PKCertificateAuthentication`) |
| PHP | PKI Express (`Authentication`) |
| Python | PKI Express (`Authentication`) |
| Node.js | Rest PKI (`Authentication`) |
| Java | PKI Express (`Authentication`) |

PKI SDK and Rest PKI give you the hash to sign. PKI Express gives you the nonce itself, so those
examples hash it with the digest algorithm PKI Express names.

**C#**

```csharp title="Controllers/CloudLoginController.cs"
using Lacuna.Cloudhub.Api;
using Lacuna.Cloudhub.Client;
using Lacuna.Pki;
using Lacuna.Pki.Stores;
using Microsoft.AspNetCore.Mvc;

public class CloudLoginController : Controller {

    private static readonly CloudhubClient cloudhub = new CloudhubClient(
        "https://cloudhub.lacunasoftware.com/",
        Environment.GetEnvironmentVariable("CLOUDHUB_API_KEY"));

    // GET /CloudLogin/Complete?session=…: the redirect URI
    public async Task<IActionResult> Complete(string session) {
        if (string.IsNullOrEmpty(session)) {
            return RedirectToAction("Index"); // not authorized
        }

        // 1. The user's certificate, from CloudHub
        byte[] certificate = await cloudhub.GetCertificateAsync(session);

        // 2. PKI SDK creates a nonce. Store nonces in your database in production.
        var auth = new PKCertificateAuthentication(new FileSystemNonceStore(nonceFolder));
        byte[] nonce = auth.Start();

        // 3. CloudHub signs the nonce's hash with the user's key
        var signature = await cloudhub.SignHashAsync(new SignHashRequest {
            Session = session,
            Hash = PKCertificateAuthentication.DigestAlgorithm.ComputeHash(nonce),
            DigestAlgorithmOid = PKCertificateAuthentication.DigestAlgorithm.Oid,
        });

        // 4. PKI SDK checks the signature and validates the certificate
        var validationResults = auth.Complete(nonce, certificate, signature,
            TrustArbitrators.PkiBrazil, out PKCertificate userCert);
        if (!validationResults.IsValid) {
            return View("Failed", validationResults);
        }

        // 5. Find the user by userCert.PkiBrazil.CPF and sign them in
        return RedirectToAction("Index", "Home");
    }
}
```

**PHP**

```php title="complete.php"
<?php
require __DIR__ . '/../vendor/autoload.php';

use Lacuna\Cloudhub\CloudhubClient;
use Lacuna\Cloudhub\SignHashRequest;
use Lacuna\PkiExpress\Authentication;

$cloudhub = new CloudhubClient('https://cloudhub.lacunasoftware.com/', getenv('CLOUDHUB_API_KEY'));

$session = $_GET['session'] ?? null;
if (empty($session)) {
    header('Location: /login.php'); // not authorized
    exit;
}

// 1. The user's certificate, from CloudHub (base64)
$certificate = $cloudhub->getCertificateAsync($session);

// 2. PKI Express creates a nonce, base64-encoded
$auth = new Authentication();
$start = $auth->start();

// 3. CloudHub signs the nonce's hash with the user's key
$phpHashNames = [
    '2.16.840.1.101.3.4.2.1' => 'sha256',
    '2.16.840.1.101.3.4.2.2' => 'sha384',
    '2.16.840.1.101.3.4.2.3' => 'sha512',
];
$hash = hash($phpHashNames[$start->digestAlgorithmOid], base64_decode($start->nonce), true);
$signature = $cloudhub->signHashAsync(new SignHashRequest(
    $session,
    base64_encode($hash),
    null,
    $start->digestAlgorithmOid
));

// 4. PKI Express checks the signature and validates the certificate
$auth->setNonce($start->nonce);
$auth->setCertificateBase64($certificate);
$auth->setSignature($signature);
$result = $auth->complete();
if (!$result->validationResults->isValid()) {
    http_response_code(403);
    echo nl2br(htmlspecialchars((string) $result->validationResults));
    exit;
}

// 5. Find the user by $result->certificate->pkiBrazil->cpf and sign them in
```

**Python**

```python title="cloud_login.py"
import base64
import hashlib

from flask import Blueprint, abort, redirect, request, url_for
from pkiexpress import Authentication

from cloudhub import call_cloudhub  # the helper from "Get started"

blueprint = Blueprint('cloud_login', __name__, url_prefix='/cloud-login')

HASH_FUNCTIONS = {
    '2.16.840.1.101.3.4.2.1': hashlib.sha256,
    '2.16.840.1.101.3.4.2.2': hashlib.sha384,
    '2.16.840.1.101.3.4.2.3': hashlib.sha512,
}


@blueprint.route('/complete')
def complete():
    cloudhub_session = request.args.get('session')
    if not cloudhub_session:
        return redirect(url_for('.index'))  # not authorized

    # 1. The user's certificate, from CloudHub (base64)
    certificate = call_cloudhub('GET', '/api/sessions/certificate',
                                params={'session': cloudhub_session})

    # 2. PKI Express creates a nonce, base64-encoded
    auth = Authentication()
    start = auth.start()

    # 3. CloudHub signs the nonce's hash with the user's key
    digest = HASH_FUNCTIONS[start.digest_algorithm_oid](base64.b64decode(start.nonce)).digest()
    signature = call_cloudhub('POST', '/api/sessions/sign-hash', json={
        'session': cloudhub_session,
        'hash': base64.b64encode(digest).decode('ascii'),
        'digestAlgorithmOid': start.digest_algorithm_oid,
    })

    # 4. PKI Express checks the signature and validates the certificate
    auth.nonce_base64 = start.nonce
    auth.certificate_base64 = certificate
    auth.signature_base64 = signature
    result = auth.complete()
    if not result.validation_results.is_valid:
        abort(403, str(result.validation_results))

    # 5. Find the user by result.certificate.pki_brazil.cpf and sign them in
    return redirect(url_for('home'))
```

**Node.js**

```js title="routes/cloud-login.js"
const express = require('express');
const { CloudHubClient } = require('cloudhub-client');
const {
  Authentication,
  CompleteAuth,
  RestPkiClient,
  StandardSecurityContexts,
} = require('restpki-client');

const cloudhub = new CloudHubClient('https://cloudhub.lacunasoftware.com/', process.env.CLOUDHUB_API_KEY);
const restPki = new RestPkiClient('https://pki.rest/', process.env.RESTPKI_ACCESS_TOKEN);
const router = express.Router();

// GET /cloud-login/complete?session=…: the redirect URI
router.get('/complete', async (req, res, next) => {
  try {
    const { session } = req.query;
    if (!session) {
      return res.redirect('/cloud-login'); // not authorized
    }

    // 1. The user's certificate, from CloudHub (base64)
    const certificate = await cloudhub.getCertificateAsync(session);

    // 2. Rest PKI starts the authentication and returns the hash to sign
    const auth = new Authentication(restPki);
    const start = await auth.start(StandardSecurityContexts.PKI_BRAZIL);

    // 3. CloudHub signs it with the user's key
    const signature = await cloudhub.signHashAsync({
      session,
      hash: start.toSignHash,
      digestAlgorithmOid: start.digestAlgorithmOid,
    });

    // 4. Rest PKI checks the signature and validates the certificate
    const result = await auth.complete(start.token, new CompleteAuth({ signature, certificate }));
    if (!result.validationResults.isValid()) {
      return res.status(403).send(result.validationResults.toString());
    }

    // 5. Find the user by result.certificate.pkiBrazil.cpf and sign them in
    res.redirect('/');
  } catch (err) {
    next(err);
  }
});

module.exports = router;
```

**Java**

```java title="CloudLoginController.java"
import cloudhub.SessionsApi;
import cloudhub.client.CloudhubClient;
import cloudhub.client.model.SignHashRequest;
import com.lacunasoftware.pkiexpress.AuthCompleteResult;
import com.lacunasoftware.pkiexpress.AuthStartResult;
import com.lacunasoftware.pkiexpress.Authentication;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

@Controller
@RequestMapping("/cloud-login")
public class CloudLoginController {

    private static final Map<String, String> HASH_NAMES = new HashMap<>();
    static {
        HASH_NAMES.put("2.16.840.1.101.3.4.2.1", "SHA-256");
        HASH_NAMES.put("2.16.840.1.101.3.4.2.2", "SHA-384");
        HASH_NAMES.put("2.16.840.1.101.3.4.2.3", "SHA-512");
    }

    private final SessionsApi cloudhub = new SessionsApi(
        new CloudhubClient("https://cloudhub.lacunasoftware.com", System.getenv("CLOUDHUB_API_KEY")));

    // GET /cloud-login/complete?session=…: the redirect URI
    @GetMapping("/complete")
    public String complete(@RequestParam(required = false) String session) throws Exception {
        if (session == null || session.isEmpty()) {
            return "redirect:/cloud-login"; // not authorized
        }

        // 1. The user's certificate, from CloudHub (base64)
        String certificate = unquote(cloudhub.apiSessionsCertificateGet(session));

        // 2. PKI Express creates a nonce, base64-encoded
        Authentication auth = new Authentication();
        AuthStartResult start = auth.start();

        // 3. CloudHub signs the nonce's hash with the user's key
        byte[] nonce = Base64.getDecoder().decode(start.getNonce());
        byte[] hash = MessageDigest.getInstance(HASH_NAMES.get(start.getDigestAlgorithmOid())).digest(nonce);
        SignHashRequest signRequest = new SignHashRequest();
        signRequest.setSession(session);
        signRequest.setHash(hash);
        signRequest.setDigestAlgorithmOid(start.getDigestAlgorithmOid());
        String signature = unquote(cloudhub.apiSessionsSignHashPost(signRequest));

        // 4. PKI Express checks the signature and validates the certificate
        auth.setNonce(start.getNonce());
        auth.setCertificateBase64(certificate);
        auth.setSignature(signature);
        AuthCompleteResult result = auth.complete();
        if (!result.getValidationResults().isValid()) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, result.getValidationResults().toString());
        }

        // 5. Find the user by result.getCertificate().getPkiBrazil().getCpf() and sign them in
        return "redirect:/";
    }

    // 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("\"", "");
    }
}
```

## Matching the certificate to a user

The validated certificate identifies a person, not an account in your application. Map it the way
your business requires; with ICP-Brasil certificates, the CPF (e-CPF) or the CNPJ and the responsible
person's CPF (e-CNPJ) are the usual keys. If you created the session with an identifier, also check
that the certificate carries that identifier: the user may have signed in at the provider with
another account.

**Demos and samples**

- Authenticate with a cloud certificate using CloudHub: [.NET](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/dotnet/mvc/PkiSuiteAspNetMvcSample/Controllers/AuthenticationCloudHubSdkController.cs) · [Node.js](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/nodejs/expressmvc/routes/authentication-cloudhub.js)
