Authentication
FluxiQ PIX uses two authentication mechanisms: JWT tokens for internal API access and mTLS (mutual TLS) with ICP-Brasil certificates for BACEN communication.
API Authentication (JWT)
All internal API endpoints require a Bearer token in the Authorization header.
Login
POST /api/v1/auth/loginRequest Body:
{
"email": "admin@institution.com.br",
"password": "your-password"
}Response: 200 OK
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "usr-123",
"email": "admin@institution.com.br",
"name": "Admin User",
"roles": ["admin", "operator"]
},
"expires_in": 3600
}Using the Token
Include the JWT token in all subsequent requests:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
http://localhost:4003/api/v1/keysToken Refresh
Refresh an expired access token using the refresh token:
POST /api/v1/auth/refreshRequest Body:
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Response: 200 OK
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 3600
}Token Lifecycle
| Token | Lifetime | Storage |
|---|---|---|
| Access token | 1 hour | Memory (frontend state) |
| Refresh token | 7 days | HttpOnly cookie or secure storage |
JWT Claims
The access token contains the following claims:
{
"sub": "usr-123",
"email": "admin@institution.com.br",
"roles": ["admin", "operator"],
"ispb": "12345678",
"iat": 1705312800,
"exp": 1705316400,
"iss": "fluxiq-pix"
}Role-Based Access Control
| Role | Description | Permissions |
|---|---|---|
admin | Full system access | All operations |
operator | Day-to-day operations | Payments, keys, monitoring |
compliance | Compliance and audit | Infractions, reports, audit logs |
viewer | Read-only access | View dashboards and reports |
api_client | Programmatic access | API operations only |
BACEN mTLS Authentication
Communication with BACEN DICT and SPI APIs uses mutual TLS (mTLS) with ICP-Brasil digital certificates. Both the client and server authenticate each other using X.509 certificates.
How mTLS Works
┌──────────────┐ ┌──────────────┐
│ FluxiQ PIX │ │ BACEN │
│ (Client) │ │ (Server) │
└──────┬───────┘ └──────┬───────┘
│ │
│ 1. ClientHello │
│────────────────────────────────────────>│
│ │
│ 2. ServerHello + Server Certificate │
│<────────────────────────────────────────│
│ │
│ 3. Client validates server cert │
│ (BACEN CA chain) │
│ │
│ 4. Client Certificate │
│────────────────────────────────────────>│
│ │
│ 5. Server validates client cert │
│ (ICP-Brasil CA chain) │
│ │
│ 6. Encrypted channel established │
│<══════════════════════════════════════>│
│ │Certificate Types
ICP-Brasil A1 Certificate
- Software-stored certificate (PKCS#12 / PFX format)
- Valid for 1 year
- Private key stored in file system
- Suitable for server-to-server communication
ICP-Brasil A3 Certificate
- Hardware-stored certificate (HSM or smart card)
- Valid for up to 5 years
- Private key never leaves the hardware device
- Higher security, required for production by some institutions
Certificate Setup
1. Obtain Your Certificate
Request an ICP-Brasil e-CNPJ certificate from an accredited Certificate Authority (AC):
- Certisign
- Serasa Experian
- Valid Certificadora
- Soluti
The certificate must contain your institution's CNPJ and the ISPB registered with BACEN.
2. Extract PEM Files
If you received a PFX/PKCS#12 file, extract the PEM components:
# Extract the client certificate
openssl pkcs12 -in certificate.pfx -clcerts -nokeys -out client.pem
# Extract the private key
openssl pkcs12 -in certificate.pfx -nocerts -nodes -out client_key.pem
# Extract the CA chain
openssl pkcs12 -in certificate.pfx -cacerts -nokeys -chain -out ca_chain.pem
# Set appropriate permissions
chmod 600 client_key.pem
chmod 644 client.pem ca_chain.pem3. Install BACEN Root CA
Download BACEN's root CA certificate for server verification:
# Download BACEN CA (check BACEN's official documentation for the current URL)
curl -o bacen_ca.pem https://www.bcb.gov.br/content/estabilidadefinanceira/pix/bacen-ca.pem4. Configure the Application
# config/runtime.exs
config :shared, Shared.Bacen.Client,
# Client certificate and key
cert_path: System.get_env("CERT_PATH"),
key_path: System.get_env("KEY_PATH"),
# CA chain for server verification
ca_path: System.get_env("CA_PATH"),
bacen_ca_path: System.get_env("BACEN_CA_PATH"),
# TLS options
tls_versions: [:"tlsv1.2", :"tlsv1.3"],
# Connection settings
pool_size: 50,
max_overflow: 25,
timeout: 10_0005. Verify the Connection
# Test mTLS connection to BACEN homologation
openssl s_client \
-connect dict-h.pi.rsfn.net.br:16522 \
-cert client.pem \
-key client_key.pem \
-CAfile ca_chain.pem \
-verify 4 \
-tls1_2Implementation Details
The Shared BACEN client handles mTLS at the Erlang :ssl level:
defmodule Shared.Bacen.Client do
@moduledoc "mTLS-authenticated HTTP client for BACEN APIs"
def request(method, path, body \\ nil, opts \\ []) do
url = base_url() <> path
headers = [
{"Content-Type", "application/json"},
{"Accept", "application/json"},
{"PI-RequestingParticipant", ispb()},
{"PI-PaymentServiceProvider", ispb()}
]
ssl_opts = [
certfile: String.to_charlist(config(:cert_path)),
keyfile: String.to_charlist(config(:key_path)),
cacertfile: String.to_charlist(config(:ca_path)),
verify: :verify_peer,
depth: 4,
customize_hostname_check: [
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
],
versions: [:"tlsv1.2", :"tlsv1.3"]
]
Finch.build(method, url, headers, encode_body(body))
|> Finch.request(Shared.Finch, receive_timeout: 10_000, pool_timeout: 5_000)
|> handle_response()
end
endJWS Message Signing
In addition to mTLS transport security, SPI messages require JWS (JSON Web Signature) signing:
defmodule Shared.Bacen.JwsSigner do
@moduledoc "Signs messages with JWS for BACEN SPI"
def sign(payload, opts \\ []) do
private_key = load_private_key(config(:key_path))
header = %{
"alg" => "PS256",
"typ" => "JWT",
"kid" => key_id(private_key)
}
{_, signed} = JOSE.JWS.sign(private_key, Jason.encode!(payload), header)
signed
end
def verify(jws_token) do
public_key = load_bacen_public_key()
{verified, payload, _} = JOSE.JWS.verify(public_key, jws_token)
if verified, do: {:ok, Jason.decode!(payload)}, else: {:error, :invalid_signature}
end
endXML Digital Signatures
ISO 20022 XML messages sent to SPI require XML Digital Signatures (XMLDSig):
defmodule Shared.Bacen.XmlSigner do
def sign_xml(xml_document) do
private_key = load_private_key(config(:key_path))
certificate = load_certificate(config(:cert_path))
xml_document
|> add_signature_element(private_key, certificate)
|> canonicalize()
|> compute_digest()
|> sign_with_key(private_key)
end
endSecurity Best Practices
Certificate Management
- Store private keys with restricted file permissions (
chmod 600) - Never commit certificates to version control
- Use environment variables or secrets management for certificate paths
- Monitor certificate expiration and renew 30 days before expiry
- Keep backup copies of certificates in a secure vault
Network Security
- All BACEN communication occurs over the RSFN network
- Enable network-level firewalling to restrict outbound connections
- Use a dedicated network interface for BACEN traffic
- Log all mTLS handshake failures for security monitoring
Key Rotation
# Implement certificate rotation without downtime
defmodule Shared.Bacen.CertRotation do
def rotate(new_cert_path, new_key_path) do
# Verify new certificate is valid
{:ok, _} = verify_certificate(new_cert_path, new_key_path)
# Update configuration atomically
Application.put_env(:shared, :cert_path, new_cert_path)
Application.put_env(:shared, :key_path, new_key_path)
# Restart connection pool to use new certificates
Supervisor.restart_child(Shared.Supervisor, Shared.Bacen.Pool)
:ok
end
endAudit Trail
All authentication events are logged for compliance:
- Login attempts (success and failure)
- Token issuance and refresh
- mTLS handshake results
- Certificate validation outcomes
- Permission denied events