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_required decorators) 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 the login_required / role_required decorators can locate the auth instance for the current app. Without it they raise a RuntimeError. Use role_required("admin", "editor") for any-of, or role_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