Skip to main content

Python client SDK for Signward Identity Server — OIDC authentication for FastAPI, Flask, and other Python apps.

Project description

signward-idserver-client

Python client SDK for Signward Identity Server — OIDC authentication for FastAPI, Flask, and other Python apps. Open source, no Microsoft-stack dependency.

Features

  • OIDC Authorization Code flow (with optional PKCE)
  • Local JWT validation via the server's JWKS (HS256 today, algorithm-agnostic)
  • 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 helpers) and Flask (Blueprint + login_required / role_required decorators) integrations
  • MIT licensed

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)

The [flask] extra ships a Blueprint (/login, /callback, /logout) plus login_required / role_required decorators:

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="...",
)

auth = IdServerAuth(client, post_login_redirect="/")
app.register_blueprint(auth.blueprint, url_prefix="/auth")
init_app(app, auth)                   # required: wires the decorators to this app


@app.route("/profile")
@login_required
def profile():
    user = auth.current_user()
    return {"id": str(user.user_id), "email": user.email, "roles": user.roles}


@app.route("/admin")
@role_required("admin")
def admin():
    return "Admin area"

Register https://myapp.com/auth/callback as a redirect URI on your Signward client.

Quickstart — Server-side login flow

For a Python web app that wants to perform 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
))

Using your own HTTP client

If you want 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:
    ...

License

MIT

Project details


Download files

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

Source Distribution

signward_idserver_client-1.0.1.tar.gz (14.0 kB view details)

Uploaded Source

Built Distribution

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

signward_idserver_client-1.0.1-py3-none-any.whl (15.7 kB view details)

Uploaded Python 3

File details

Details for the file signward_idserver_client-1.0.1.tar.gz.

File metadata

  • Download URL: signward_idserver_client-1.0.1.tar.gz
  • Upload date:
  • Size: 14.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.12

File hashes

Hashes for signward_idserver_client-1.0.1.tar.gz
Algorithm Hash digest
SHA256 0c73d964b0de2736c2b365f94a2837ce9eee559d91a78ad44e2b86d0e4e18591
MD5 a22122ff703b885859805e7b4dea621c
BLAKE2b-256 57788c3fb783b7c1bf0dd5a067a4df3c1f68a542a4c200efd62564efb89a8d1d

See more details on using hashes here.

File details

Details for the file signward_idserver_client-1.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for signward_idserver_client-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a182f709b5095f8aced381aa8cd33cff4fe961a5df467d3f9eeb0757edb262ef
MD5 c8dda50d79c60e31c39767e6a525f28c
BLAKE2b-256 1e8a1c3e0320d77b1afb0a4bacb72a9bf5104053cef84dd7cae88a3c20fb242c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page