Skip to main content

zoreal-oauth2

Login with ZOREAL for Python backends: the relying-party half of the flow that @zoreal/oauth2-react starts in the browser.

The browser SDK runs the pairing (QR or app link), and hands your frontend an authorization code plus the code_verifier and nonce it generated. Your frontend posts all three to your backend, and this package does the rest: the code exchange with your client authentication, ES256 verification of the ID token against the provider's JWKS, and the /userinfo read for personal claims.

zoreal-oauth2 (this package)   your backend: exchange, verify, userinfo
@zoreal/oauth2-react           your frontend: the button, the QR, the polling

Install

# until the package is on PyPI, install from the git source:
pip install "zoreal-oauth2 @ git+https://github.com/Bynn-Intelligence/zoreal-oauth2-python"

Python >= 3.9. One dependency: PyJWT[crypto]. Framework-agnostic: the same client works from Django, FastAPI, Flask, or anything else that can receive a POST. The package ships type hints and a py.typed marker.

Quick start

Build one client at boot and share it; it is thread-safe.

import os
from zoreal_oauth2 import ClientSecretBasic, ZorealOAuth2Client

ZOREAL_OAUTH = ZorealOAuth2Client(
    client_id=os.environ["ZOREAL_CLIENT_ID"],                    # ast_...
    auth=ClientSecretBasic(os.environ["ZOREAL_CLIENT_SECRET"]),
    issuer=os.environ.get("ZOREAL_ISSUER", "https://id.zoreal.com"),
    cache=None,  # optional, for the JWKS; Django's cache object fits as-is
)

The endpoint your frontend posts to (any framework; the body is the {code, code_verifier, nonce} the browser SDK handed over):

login = ZOREAL_OAUTH.authenticate(
    code=payload["code"],
    code_verifier=payload["code_verifier"],  # PKCE is mandatory; the SDK hands it over
    nonce=payload["nonce"],                  # binds the ID token to this login
)

login.sub            # "TC5X-JN7G-YTSE-6E63" — pairwise, stable for YOUR domain
login.acr            # "zoreal.live" | "zoreal.device" | "zoreal.session"
login.assurance      # uniqueness basis, verification month, chip liveness, trust tier
login.email          # from /userinfo, when your client has the email scope
login.email_verified
login.name           # from /userinfo, profile.name scope

Account matching, the shape that works:

user = User.objects.filter(provider="zoreal", uid=login.sub).first()
if user is None:
    if login.email_verified:  # claim, don't collide
        user = User.objects.filter(email=login.email).first()
    user = user or User(email=login.email)
    user.provider, user.uid = "zoreal", login.sub
    user.save()

Client authentication

Four methods, one class each. Use the one your client is registered with in the ZOREAL dashboard.

from zoreal_oauth2 import (
    ClientSecretBasic, NoAuth, PrivateKeyJwt, TlsClientAuth, ZorealOAuth2Client,
)

# A public client: no secret, no key. PKCE is the only proof, which is why a
# public client can only ever have been granted Tier A scopes.
ZorealOAuth2Client(client_id, auth=NoAuth())  # auth=None means the same

# Confidential, shared secret. The secret travels as HTTP Basic, never as a
# form field.
ZorealOAuth2Client(client_id, auth=ClientSecretBasic(client_secret))

# Confidential, private_key_jwt (RFC 7523). The library builds and signs a
# fresh 55-second assertion per exchange (iss/sub = client_id, aud = the token
# endpoint, single-use jti); your private key never travels. A P-256 key signs
# ES256 (preferred — it is the same key shape ZOREAL certifies), an RSA key
# signs RS256. PEM string, private JWK dict, or a cryptography key object.
ZorealOAuth2Client(client_id, auth=PrivateKeyJwt(pem_or_key, kid="rp-key-1"))

# Mutual TLS: the certificate and key are loaded into the TLS context for
# every call this client makes. Registrable, but the provider does not accept
# it at the token endpoint yet — the 501 it answers surfaces as an
# ExchangeError with that status, verbatim, rather than being papered over.
ZorealOAuth2Client(client_id, auth=TlsClientAuth("cert.pem", "key.pem"))

What each call does

Call What happens
authenticate(code, code_verifier, nonce=None) exchange + verify_id_token, returns a Login
exchange(code, code_verifier) POST {issuer}/token with your client authentication
verify_id_token(id_token, nonce=None) ES256 against {issuer}/jwks, checks iss, aud, exp, and nonce when given
userinfo(access_token) GET {issuer}/userinfo with the Bearer token
Login.userinfo the above, once, memoized; {} when there is no access token

Login reads the verified claims for you: sub, acr, amr, assurance, age_over(threshold), nationality from the ID token; email, email_verified, name, given_name, family_name, birthdate, document_type, document_number, issuing_country, document_expires_on and portrait from /userinfo, fetched lazily.

Errors: ConfigurationError, ExchangeError (carries the provider's OAuth error code and reason, verbatim, plus the HTTP status), VerificationError, UserinfoError. A returning user matched on sub can survive a caught UserinfoError; a signup that needs the email cannot. Token values never appear in error messages.

Things worth knowing before you integrate

  • The ID token never carries personal data. sub, timing, acr/amr, the assurance block, and — if registered — age_over_* booleans and nationality. Email, names, birthdate and document fields come only from /userinfo, which is why authenticate alone is not enough for a signup.
  • The access token lives 10 minutes. Read /userinfo while handling the login; do not store the token for later.
  • sub is pairwise per verified domain. It is the right account key and it is derived from your registered sector: changing your asset's domain rotates every sub you have stored. Plan domain changes as a migration.
  • ES256 only. The provider signs with nothing else, and this package refuses other algorithms rather than negotiating.
  • Always pass the nonce through. The SDK generates it and gives it to your frontend in onSuccess; without it your backend cannot tell a substituted ID token from the real one.
  • Email is a deliberate choice. It is a Tier B scope precisely because a shared email defeats the unlinkability the pairwise sub provides. Request it because you need it, not because the checkbox is familiar.
  • Sandbox clients accept localhost origins; production clients do not. Registration lives in the ZOREAL dashboard on the asset's OAuth2 tab; Tier B scopes (email, profile.*) need a confidential client on a verified domain.
  • Pick the client authentication your posture needs. A public client (NoAuth) is for exchanges that happen where a secret cannot live; ClientSecretBasic is the ordinary confidential setup; PrivateKeyJwt replaces the shared secret with proof of possession of a key that never travels, and is the method ZOREAL's certificate path is built around; TlsClientAuth is registrable but the provider answers 501 at the token endpoint today, and this library surfaces that rather than faking it.
  • profile.portrait is registrable but not served yet. Login.portrait exists so your code does not change when it ships; expect None until then.

Development against a local provider

Point issuer at your provider instance. The issuer value must match the iss inside the tokens exactly — it is compared, not normalized.

Development on this package

python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest

The tests are offline: keys are generated in-process and the JWKS fetch is stubbed.

The ZOREAL OAuth2 library family

Repository Package Role
zoreal-oauth2-react @zoreal/oauth2-react (npm) React frontend: the button, the QR, the polling
zoreal-oauth2-js @zoreal/oauth2-js (npm) Framework-free browser core
zoreal-oauth2-react-native @zoreal/oauth2-react-native (npm) React Native frontend
zoreal-oauth2-node @zoreal/oauth2-node (npm) Node.js backend
zoreal-oauth2-ruby zoreal-oauth2 (RubyGems) Ruby backend
zoreal-oauth2-python zoreal-oauth2 (PyPI) Python backend
zoreal-oauth2-php zoreal/oauth2 (Packagist) PHP backend
zoreal-oauth2-go github.com/Bynn-Intelligence/zoreal-oauth2-go Go backend
zoreal-oauth2-java com.zoreal:oauth2 (Maven Central) JVM backend
zoreal-oauth2-dotnet Zoreal.OAuth2 (NuGet) .NET backend

License

MIT.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zoreal_oauth2-0.1.1.tar.gz (18.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zoreal_oauth2-0.1.1-py3-none-any.whl (15.9 kB view details)

Uploaded Python 3

File details

Details for the file zoreal_oauth2-0.1.1.tar.gz.

File metadata

  • Download URL: zoreal_oauth2-0.1.1.tar.gz
  • Upload date:
  • Size: 18.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for zoreal_oauth2-0.1.1.tar.gz
Algorithm Hash digest
SHA256 475957274f37fcc86ddea258df5dfca0a2e81f8dc5f758c6c4266dfbccfd8e36
MD5 a99987532125f58fdff2d825e3fe25fa
BLAKE2b-256 cace35eedf65835a92f5803791c8e1c56433ec1a2a5c22ae2524f0edb0d3b9ff

See more details on using hashes here.

File details

Details for the file zoreal_oauth2-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: zoreal_oauth2-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 15.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for zoreal_oauth2-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 102a153fceb762537e949d6228da0a9d92310c1ce991fae85dfee7772e76a3e2
MD5 6a4075610bbd14696597b236603c25b7
BLAKE2b-256 4876c688cbbfdc5b885f220e3bf362db4ce53364968c30fbd49f3e45f07a0cec

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

This release

0.1.1 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page