IdP SDK — Python
Add AuthPI login to Python backends with authpi-idp — the OIDC authorization-code flow with PKCE, token refresh, and per-organization authorization checks.
Last updated 2026-09-15
authpi-idp is the official Python SDK for authenticating users against your AuthPI issuer: it drives the OIDC authorization-code flow with PKCE, exchanges and refreshes tokens, and gives you an authenticated agent object with the user’s identity and per-organization permissions.
Requirements: Python 3.11+. Use IdpClient for async applications or IdpClientSync for synchronous applications.
Install
pip install authpi-idp
Initialize
from authpi_idp import IdpClient
idp = IdpClient(
issuer_url="https://idp.authpi.com/i_4r8w2k9m5x1p7q3e6t0y2u4i8",
client_id="c_xxx",
client_secret="...", # omit for public clients (SPAs, native apps)
redirect_uri="https://app.example.com/callback",
# resources=["https://api.example.com/reports"], # after configuring RFC 8707 policy
)
After adding an exact resource to both the issuer catalog and registered-client allowlist, optional resources values are serialized as repeated RFC 8707 resource parameters during authorization, code exchange, and refresh.
The login flow
1. Send the user to AuthPI. create_authorization_url is synchronous and generates the PKCE verifier, state, and nonce for you — store the returned object in the user’s session, then redirect:
auth = idp.create_authorization_url(scopes=["openid", "profile", "email"])
session["oauth"] = auth.model_dump()
return redirect(auth.url)
2. Handle the callback. Use the stored authorization object to validate state, exchange the code, require an ID token, and verify its nonce:
from authpi_idp import AuthorizationUrl
agent = await idp.exchange_callback(callback_url, AuthorizationUrl(**session["oauth"]))
session["tokens"] = agent.tokens.model_dump()
AuthorizationUrl includes the effective resources list. Store the whole model: exchange_callback repeats that exact list during code exchange. To select a refresh subset explicitly, pass resources=["https://api.example.com/reports"] to refresh or create_agent. An empty list omits the parameter. Automatic refresh from create_agent also omits resource unless that call explicitly supplies resources, even when the client has an authorization default; under an optional policy the server then retains the refresh session’s selected set. A policy with required: true needs explicit create_agent resources and rejects omission. TokenSet intentionally has no resource field.
3. Use the agent. It carries the user’s identity and organization memberships, with an authorization helper that checks scopes within an organization:
if agent.has_access_in("org_0kfz3m8q1w5e9r2t6y4u7i3o5", "write", "projects"):
... # the user can write to projects in that organization
Native applications and CLI tools
Register a native client in your issuer with confidential: false, settings.openid.application_type: "native", and http://127.0.0.1/callback in settings.openid.redirect_uris.
Bind your callback listener to 127.0.0.1 on an available port before creating the SDK client. In this example, bound_port is the port reported by that listener, and callback_url is the full callback URL it receives:
idp = IdpClient(
issuer_url="https://idp.authpi.com/i_4r8w2k9m5x1p7q3e6t0y2u4i8",
client_id="c_xxx",
redirect_uri=f"http://127.0.0.1:{bound_port}/callback",
)
auth = idp.create_authorization_url(scopes=["openid", "profile"])
# Open auth.url in the system browser; retain idp and auth until the callback.
agent = await idp.exchange_callback(callback_url, auth)
Omit the client secret; the SDK generates S256 PKCE automatically. Reuse this client for the code exchange so it sends the identical configured URI, including its port, path, and query. If you reconstruct the client, use that same URI, persisted alongside auth. See the matching rules.
For an OS-registered private-use callback, set redirect_uri to a registered URI such as com.example.app:/callback instead. IdpClientSync supports the same flow without await. Post-logout redirects always require an exact registered port.
Sessions and refresh
Rebuild an agent from stored tokens on subsequent requests — create_agent refreshes expired access tokens automatically and hands you the rotated tokens through on_refresh:
agent = await idp.create_agent(
session["tokens"],
on_refresh=lambda new_tokens: session.update({"tokens": new_tokens}),
on_refresh_error=lambda error: handle_session_expired(error),
)
Refresh tokens rotate on use, so always persist what on_refresh gives you. The callback runs immediately after the successful token response is parsed and before optional user-info hydration. If hydration fails afterward, the method raises but does not invoke on_refresh_error; a later create_agent call rehydrates the persisted token state and verifies its SDK-managed subject hint. Explicit refresh(agent, on_refresh=...) calls support the same early persistence callback. Organization claims in refreshed tokens are recomputed at every refresh — membership changes propagate within the access-token TTL (the propagation contract).
Next steps
- Server-side auth with the TypeScript SDK — the same flow, step by step (the concepts transfer directly)
- Token claims reference — what’s inside
agent.tokens - Validate tokens in your API — verifying these tokens in downstream services
- Package on PyPI — full README, framework integration notes, and advanced options