Secure authentication on POSOS API
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 (
.jsonfile) - 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 theissandsubof the assertionkeyId— key identifier, to be placed in thekidheader of the assertionkey— RSA private key used to sign the assertion (strictly secret)
Issuer per environment
Issuer
https://zitadel.preprod.posos.coThe token endpoint is always <issuer>/oauth/v2/token.
Scopes
Three scopes must be requested when exchanging the assertion for an access token:
| Scope | Purpose |
|---|---|
openid | Standard OpenID Connect scope |
urn:zitadel:iam:org:project:id:PROJECT_ID:aud | Adds the POSOS project to the audience of the token |
urn:zitadel:iam:org:projects:roles | Includes 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.
Project ID
380745938064375828Requesting 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
- A list of libraries allowing you to build and sign a JWT is available here:
JWT.IO - JSON Web Tokens Libraries
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 requestsSending authenticated requests
The access token must be attached to the request header in the Authorization header in the format Bearer <token content>.
curl --request GET --url 'https://api.preprod.posos.co/[...]' --header 'Authorization: Bearer <token>'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.