Secure authentication on Posos API

Get an access token for the Posos APIs from a service account private key (OAuth 2, Private Key JWT).

Posos API is protected by an API Gateway, ensuring secure access to APIs and data. This interface uses the OAuth 2 and OpenID Connect protocols to authorize access, and validates the identity of the caller using a private key. This key is transmitted to you by POSOS in the form of a .json file and is strictly secret. It must be able to be changed quickly in the event of revocation.

In principle, the caller builds an assertion signed with its private key and sends it to the Posos authentication service, which returns an access token. This proof of authentication is then attached in the header of subsequent requests, and validated by the gateway without any further call to the authentication service (offline validation through JWKS).

The reference documentation can be found at the following address:

Authenticate with Private Key JWT

The caller needs two things:

  • its private key (.json file)
  • the issuer URL specific to the called environment. It is stable for each environment (preprod, production).

Unlike the legacy authentication (Google IAP), there is no OAuth client identifier or target audience to request from POSOS: the audience of the assertion is the issuer itself.

Private key

The .json file transmitted by POSOS has the following shape:

service-account-key.json
{
  "type": "serviceaccount",
  "keyId": "..........",
  "key": "-----BEGIN RSA PRIVATE KEY-----\n..........\n-----END RSA PRIVATE KEY-----\n",
  "userId": ".........."
}
  • userId — service account identifier, used as the iss and sub of the assertion
  • keyId — key identifier, to be placed in the kid header of the assertion
  • key — RSA private key used to sign the assertion (strictly secret)

Issuer per environment

https://zitadel.preprod.posos.co

The token endpoint is always <issuer>/oauth/v2/token.

Scopes

Three scopes must be requested when exchanging the assertion for an access token:

ScopePurpose
openidStandard OpenID Connect scope
urn:zitadel:iam:org:project:id:PROJECT_ID:audAdds the POSOS project to the audience of the token
urn:zitadel:iam:org:projects:rolesIncludes the service account roles in the token (posos:roles claim)

The urn:zitadel:iam:org:projects:roles scope is mandatory: without it, the token is still issued but carries no role, and the gateway rejects requests with a 403.

PROJECT_ID is the identifier of the POSOS project, specific to each environment. It is transmitted to you by POSOS at the same time as the key.

380745938064375828

Requesting an access token can be done with any technology capable of making an HTTP request, but most languages have libraries that build and sign the JWT assertion automatically.

Implementation examples

import json
import time

import jwt  # PyJWT
import requests

# The private key is in the .json file transmitted by POSOS
KEY_FILE = "service-account-key.json"

# Values specific to the called environment, should be provided
# by environment variables
ISSUER = "https://zitadel.preprod.posos.co"
PROJECT_ID = "380745938064375828"

SCOPE = " ".join(
    [
        "openid",
        f"urn:zitadel:iam:org:project:id:{PROJECT_ID}:aud",
        "urn:zitadel:iam:org:projects:roles",
    ]
)

with open(KEY_FILE) as f:
    key = json.load(f)

now = int(time.time())

# iss and sub are the service account identifier, aud is the
# issuer. Posos Auth caps the assertion lifetime at 1 hour.
assertion = jwt.encode(
    {
        "iss": key["userId"],
        "sub": key["userId"],
        "aud": ISSUER,
        "iat": now,
        "exp": now + 3600,
    },
    key["key"],
    algorithm="RS256",
    headers={"kid": key["keyId"]},
)

response = requests.post(
    f"{ISSUER}/oauth/v2/token",
    data={
        "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "assertion": assertion,
        "scope": SCOPE,
    },
    timeout=10,
)
response.raise_for_status()

token = response.json()["access_token"]
# token contains the access token to attach to requests

Sending authenticated requests

The access token must be attached to the request header in the Authorization header in the format Bearer <token content>. Every request must also carry the x-posos-user-id and x-posos-facility-id headers (see User and facility identification).

curl --request GET --url 'https://api.preprod.posos.co/[...]' \
  --header 'Authorization: Bearer <token>' \
  --header 'x-posos-user-id: rpps/<RPPS number>' \
  --header 'x-posos-facility-id: finess/<FINESS number>'

User and facility identification

Every call to POSOS APIs must include the following two headers, which state for which user and which facility the request is made:

HeaderIdentifiesAccepted id_type
x-posos-user-idThe userrpps, internal
x-posos-facility-idThe facilityfiness, internal

The value of each header must be of the form <id_type>/<value>, where id_type is the type of identifier and value the identifier itself:

id_typevalueExample
rppsRPPS number of the healthcare professional (11 digits)rpps/10000000001
finessFINESS number of the facility (9 characters)finess/750000523
internalYour own identifier for the user or the facilityinternal/42

With internal, value is the identifier under which your own system knows the user or the facility: it is not a POSOS internal identifier.

These headers let POSOS know, for each request and whichever API is called, which user and which facility it was made for. They come in addition to the access token and do not replace it: only the token authenticates the caller.

Token lifetime and reuse

The response of the /oauth/v2/token endpoint contains the expires_in field (in seconds), indicating how long the access token remains valid.

Reuse the same token until it expires rather than requesting a new one for each request: the identity provider rate-limits this endpoint. Plan a safety margin (for example, renew the token when less than 5 minutes of validity remain).

Verifying the token

The issued access token is a JWT: you can decode its middle part (base64url) to inspect its claims. The posos:roles claim must contain the roles granted to your service account.

echo "$ACCESS_TOKEN" | cut -d. -f2 \
  | python3 -c 'import base64,sys; s=sys.stdin.read().strip(); print(base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)).decode())' \
  | jq '."posos:roles"'

If this claim is missing, check that the urn:zitadel:iam:org:projects:roles scope is indeed part of the token request, then contact POSOS if the problem persists.

Last updated on

Posos Healthcare Platform 2026 © Posos.

On this page