Python SDK
Integrate Signward authentication into FastAPI, Flask, and other Python apps with the signward-idserver-client package.
Python SDK
The signward-idserver-client package adds Signward OIDC authentication to FastAPI,
Flask, and any other Python app. Open source (MIT), no Microsoft-stack
dependency.
- OIDC Authorization Code flow (with optional PKCE)
- Local JWT validation via the server's JWKS, with discovery + JWKS caching
- Typed Pydantic v2 user model with built-in and per-tenant custom roles
- Async-first (
httpx.AsyncClient) - First-class FastAPI (
Depends-friendly auth helpers) and Flask (Blueprint +login_required/role_requireddecorators) integrations
Install
pip install "signward-idserver-client[fastapi]" # FastAPI integration
pip install "signward-idserver-client[flask]" # Flask integration
Core-only (no framework integration):
pip install signward-idserver-client
Python 3.10+.
Quickstart — FastAPI (protecting an API)
from fastapi import FastAPI, Depends
from idserver import IdServerClient
from idserver.fastapi import IdServerAuth, CurrentUser
app = FastAPI()
idserver = IdServerClient(
authority="https://mytenant.signward.com",
client_id="my-api",
client_secret="...", # if confidential
options=None,
)
auth = IdServerAuth(idserver)
@app.get("/me")
async def me(user: CurrentUser = Depends(auth.current_user)):
return {
"id": user.user_id,
"email": user.email,
"tenant_id": user.tenant_id,
"roles": user.roles,
"custom_roles": user.custom_roles,
}
@app.get("/admin")
async def admin(user: CurrentUser = Depends(auth.require_role("admin"))):
return {"ok": True}
@app.get("/editors-or-admins")
async def editors(
user: CurrentUser = Depends(auth.require_role("admin", "editor")),
):
return {"ok": True}
@app.get("/billing-admin-only")
async def billing(
user: CurrentUser = Depends(
auth.require_role("admin", "billing", require_all=True)
),
):
return {"ok": True}
Clients call your API with a bearer token obtained from Signward:
GET /me
Authorization: Bearer eyJhbGciOi...
Quickstart — Flask (server-side login)
For a traditional Flask web app where Signward handles login. The [flask]
extra ships a ready-made Blueprint (/login, /callback, /logout) plus
login_required / role_required decorators — no need to wire the OIDC flow by
hand.
from flask import Flask
from idserver import IdServerClient
from idserver.flask import IdServerAuth, init_app, login_required, role_required
app = Flask(__name__)
app.secret_key = "change-me" # required: tokens + claims live in the session
client = IdServerClient(
authority="https://mytenant.signward.com",
client_id="my-webapp",
client_secret="...", # confidential client
)
auth = IdServerAuth(client, post_login_redirect="/")
app.register_blueprint(auth.blueprint, url_prefix="/auth")
init_app(app, auth) # wires the decorators to this app — required
@app.route("/")
def home():
user = auth.current_user()
if user is None:
return '<a href="/auth/login">Log in</a>'
return f'Hello {user.email} — <a href="/auth/logout">log out</a>'
@app.route("/profile")
@login_required # redirects to /auth/login when signed out
def profile():
user = auth.current_user()
return {"id": str(user.user_id), "email": user.email, "roles": user.roles}
@app.route("/admin")
@role_required("admin") # 403 unless the user has the admin role
def admin():
return "Admin area"
Register https://myapp.com/auth/callback as a redirect URI on your Signward
client. The Blueprint (mounted at /auth above) exposes:
| Route | Purpose |
|---|---|
GET /auth/login |
Start the OIDC flow (redirects to Signward, with PKCE) |
GET /auth/callback |
Exchange the code for tokens, store them in the session |
GET /auth/logout |
Clear the session and end the Signward session |
Required: call
init_app(app, auth)so thelogin_required/role_requireddecorators can locate the auth instance for the current app. Without it they raise aRuntimeError. Userole_required("admin", "editor")for any-of, orrole_required("admin", "billing", require_all=True)for all-of.
Quickstart — server-side login flow
For a Python web app that performs the full OIDC login flow itself:
from idserver import IdServerClient
client = IdServerClient(
authority="https://mytenant.signward.com",
client_id="my-webapp",
client_secret="...",
)
# 1) Redirect the user to the login page:
url, verifier = client.authorize_url_with_pkce(
redirect_uri="https://myapp.com/callback",
state="random-state",
)
# Store `verifier` and `state` in the session, redirect to `url`.
# 2) On callback (?code=...&state=...):
tokens = await client.exchange_code(
code,
redirect_uri="https://myapp.com/callback",
code_verifier=verifier,
)
# 3) Validate the access token locally:
user = await client.validate_token(tokens.access_token)
print(user.email, user.roles)
# 4) Or fetch userinfo remotely:
user = await client.userinfo(tokens.access_token)
# 5) Later, refresh:
new_tokens = await client.refresh_token(tokens.refresh_token)
# 6) Logout URL:
logout = await client.end_session_url(
id_token_hint=tokens.id_token,
post_logout_redirect_uri="https://myapp.com/",
)
User model
class User:
user_id: UUID | None # "sub" claim
email: str | None
email_verified: bool | None
name: str | None
given_name: str | None
family_name: str | None
tenant_id: UUID | None
roles: list[str] # built-in roles
custom_roles: list[str] # per-tenant RBAC roles
claims: dict # raw JWT / userinfo payload
def has_role(self, role: str) -> bool: ...
def has_custom_role(self, role: str) -> bool: ...
def has_any_role(self, *roles: str, include_custom: bool = True) -> bool: ...
def has_all_roles(self, *roles: str, include_custom: bool = True) -> bool: ...
Configuration
Pass values directly to IdServerClient or use an IdServerOptions dataclass
for more control:
from idserver import IdServerClient, IdServerOptions
client = IdServerClient(options=IdServerOptions(
authority="https://mytenant.signward.com",
client_id="my-app",
client_secret="...",
scopes=["openid", "profile", "email", "roles"],
audience="idserver-api", # expected JWT aud claim
issuer=None, # default: discovery.issuer
timeout=10.0,
verify_ssl=True, # set False for localhost dev
))
Sharing your own HTTP client
To share an httpx.AsyncClient with the rest of your app:
import httpx
from idserver import IdServerClient
shared_http = httpx.AsyncClient(timeout=20.0)
client = IdServerClient(
authority="...",
client_id="...",
http_client=shared_http,
)
# When constructed this way, IdServerClient will NOT close shared_http.
Error handling
All exceptions derive from idserver.IdServerError:
from idserver import IdServerError, InvalidTokenError, TokenExchangeError
try:
user = await client.validate_token(token)
except InvalidTokenError as e:
...
except TokenExchangeError as e:
print(e.error, e.description, e.status_code)
except IdServerError as e:
...
Other SDKs
- .NET SDK — ASP.NET Core, NuGet
- JavaScript / TypeScript SDK — Node.js, Express, and the browser
- PHP / Laravel SDK — Laravel middleware + any PHP app