Skip to main content

Vendor-agnostic authentication and authorization enforcement SDK

Project description

Open Shield Python SDK

CI

Vendor-agnostic authentication and authorization enforcement SDK for Python.

Open Shield lets you enforce authentication (AuthN) and authorization (AuthZ) in your Python applications without coupling to a specific identity provider. Works with Logto, Keycloak, Auth0, Entra ID, Cognito, or any OIDC-compliant provider.


Architecture

Open Shield sits as a thin, powerful layer between your identity provider and your Python backend — handling all the complexity of token validation, claim extraction, tenant resolution, and authorization enforcement so you don't have to.

How Open Shield Works

How It Works

  1. Any OIDC Identity Provider (Logto, Auth0, Keycloak, Azure Entra ID, AWS Cognito, or your own) issues a JWT token when a user or service authenticates.
  2. Open Shield SDK intercepts the incoming request in your Python application and:
    • Validates the token — verifies the signature using auto-fetched JWKS keys from the provider's OIDC discovery endpoint.
    • Maps claims — extracts user ID, email, tenant, scopes, and roles from the JWT using your configurable claim mapping (every provider names claims differently — Open Shield normalizes them).
    • Resolves the tenant — determines tenant isolation using the 3-step cascade (M2M lookup → org claim → sub fallback).
    • Detects actor type — classifies the caller as user, service, or agent.
    • Enforces authorization — checks scopes and roles before the request reaches your handler.
  3. Your Python service receives a clean, verified UserContext object — ready to use. No JWT parsing, no OIDC plumbing, no provider-specific code.

Request Authentication Flow

Request Authentication Flow

Why This Matters

  • Works with any OIDC provider — Switch from Auth0 to Keycloak? Change two environment variables. Zero code changes.
  • Works with any Python framework — First-class FastAPI support today, with Django and Flask coming soon. The core logic is pure Python with no framework dependency.
  • Built on Clean Architecture — The domain layer has zero external dependencies. All I/O (JWT decoding, OIDC discovery) happens through abstract ports implemented by swappable adapters.
  • Easy to test — Mock the TokenValidatorPort for fast unit tests without any HTTP calls.
  • Easy to extend — Implement TenantResolverPort to wire in your own database for M2M tenant lookups.

Features

  • Vendor Neutral — Works with any OIDC-compliant identity provider
  • Configurable Claim Mapping — Map any JWT claim to user/tenant/role fields
  • 3-Step Tenant Cascade — Correct isolation for individual users, SaaS orgs, and M2M clients
  • Actor Type Inference — Automatically detect users, agents, and service accounts
  • Framework Agnostic — Core logic is pure Python; first-class FastAPI support
  • Clean Architecture — Domain ↔ Adapters ↔ API layers with strict dependency inversion
  • Type Safe — Fully typed, checked with mypy
  • Automatic JWKS Rotation — Fetches and caches keys from OIDC discovery

Installation

pip install open-shield-python

Quick Start (FastAPI)

1. Configure Environment

OPEN_SHIELD_ISSUER_URL=https://your-auth-domain.com/oidc
OPEN_SHIELD_AUDIENCE=my-api-identifier

2. Add Middleware

from fastapi import FastAPI, Depends
from open_shield.api.fastapi import (
    OpenShieldMiddleware,
    get_user_context,
    get_optional_user_context,
    RequireScope,
    RequireRole,
)
from open_shield.adapters import OpenShieldConfig
from open_shield.domain.entities import UserContext

app = FastAPI()
config = OpenShieldConfig()  # Reads from env vars

app.add_middleware(OpenShieldMiddleware, config=config)

@app.get("/health")
def health():
    return {"status": "ok"}

@app.get("/users/me")
def read_current_user(ctx: UserContext = Depends(get_user_context)):
    return {
        "id": ctx.user.id,
        "email": ctx.user.email,
        "actor_type": ctx.user.actor_type,
        "tenant": ctx.tenant.tenant_id if ctx.tenant else None,
        "scopes": ctx.user.scopes,
        "roles": ctx.user.roles,
    }

@app.get("/admin/dashboard")
def admin_dashboard(ctx: UserContext = Depends(RequireScope("read:admin"))):
    return {"data": "secret"}

@app.get("/manager/reports")
def reports(ctx: UserContext = Depends(RequireRole(["manager", "admin"]))):
    return {"reports": []}

Configurable Claim Mapping

Different identity providers use different JWT claim names. Open Shield lets you configure which claims map to which identity fields — zero code changes when switching providers.

Environment Variables

Variable Description Default
OPEN_SHIELD_ISSUER_URL OIDC Issuer URL (required)
OPEN_SHIELD_AUDIENCE Expected aud claim None
OPEN_SHIELD_ALGORITHMS Allowed signing algorithms ["RS256"]
OPEN_SHIELD_USER_ID_CLAIM Claim for user ID sub
OPEN_SHIELD_EMAIL_CLAIM Claim for email email
OPEN_SHIELD_TENANT_ID_CLAIM Claim for tenant/org ID tid
OPEN_SHIELD_SCOPE_CLAIM Claim for scopes scope
OPEN_SHIELD_ROLES_CLAIM Claim for roles roles
OPEN_SHIELD_TENANT_FALLBACK Fallback when tenant claim missing none

Provider Examples

Logto
OPEN_SHIELD_ISSUER_URL=https://my-logto.com/oidc
OPEN_SHIELD_AUDIENCE=https://my-api.com
OPEN_SHIELD_TENANT_ID_CLAIM=organization_id
OPEN_SHIELD_TENANT_FALLBACK=sub    # Individual users get isolation
Keycloak
OPEN_SHIELD_ISSUER_URL=https://keycloak.com/realms/myrealm
OPEN_SHIELD_AUDIENCE=my-client-id
OPEN_SHIELD_TENANT_ID_CLAIM=tenant     # Custom claim from mapper
OPEN_SHIELD_ROLES_CLAIM=roles          # Keycloak realm_access also supported
Auth0
OPEN_SHIELD_ISSUER_URL=https://my-tenant.auth0.com/
OPEN_SHIELD_AUDIENCE=https://my-api
OPEN_SHIELD_TENANT_ID_CLAIM=org_id
OPEN_SHIELD_SCOPE_CLAIM=permissions    # Auth0 uses 'permissions' for RBAC
Azure Entra ID
OPEN_SHIELD_ISSUER_URL=https://login.microsoftonline.com/{tenant-id}/v2.0
OPEN_SHIELD_AUDIENCE=api://my-api
OPEN_SHIELD_TENANT_ID_CLAIM=tid
OPEN_SHIELD_ROLES_CLAIM=roles

Programmatic Configuration

from open_shield.adapters import OpenShieldConfig

config = OpenShieldConfig(
    ISSUER_URL="https://my-auth.com/oidc",
    AUDIENCE="https://my-api",
    TENANT_ID_CLAIM="organization_id",
    TENANT_FALLBACK="sub",
)

Tenant Resolution Cascade

Tenant isolation is critical for data security. Open Shield uses a 3-step cascade to resolve the tenant for every request — supporting individual users, SaaS organizations, and M2M service accounts.

How It Works

Step 1: M2M client (sub == client_id)  → TenantResolverPort.resolve_tenant()
Step 2: Organization claim              → OPEN_SHIELD_TENANT_ID_CLAIM
Step 3: Sub fallback                    → sub (when TENANT_FALLBACK=sub)
Use Case Tenant Source Config
Individual user sub TENANT_FALLBACK=sub
SaaS with orgs organization_id TENANT_ID_CLAIM=organization_id
M2M service Registry lookup Implement TenantResolverPort
No tenant needed None TENANT_FALLBACK=none (default)

When to Use Each Strategy

Individual users (OSS, personal tools):

OPEN_SHIELD_TENANT_FALLBACK=sub

Each user = separate dataset. Simple. Works immediately.

SaaS with organizations (teams, billing per org):

OPEN_SHIELD_TENANT_ID_CLAIM=organization_id
OPEN_SHIELD_TENANT_FALLBACK=none        # Don't fall back to sub

Multiple users share org data. Required for team features.

M2M / AI agents (client_credentials flow):

from open_shield.domain.ports import TenantResolverPort

class MyTenantResolver(TenantResolverPort):
    """Look up tenant for machine clients from your registry."""

    def resolve_tenant(self, client_id: str) -> str | None:
        # Query your DB, config, or IdP management API
        return db.get_tenant_for_client(client_id)

# Pass resolver to middleware
config = OpenShieldConfig(...)
app.add_middleware(
    OpenShieldMiddleware,
    config=config,
    tenant_resolver=MyTenantResolver(),
)

Resolution Metadata

Every resolved tenant includes traceability:

ctx.tenant.metadata["resolution"]  # "m2m_lookup" | "claim" | "sub_fallback"

Actor Type Inference

Open Shield automatically classifies the caller:

Actor Type Detection Example
user Default for human tokens Normal login flow
service sub == client_id M2M client_credentials
agent sub == client_id + "agent" role AI agent with agent role
ctx.user.actor_type  # "user" | "service" | "agent"

Optional Authentication

For routes that work with or without auth (e.g., public APIs with enhanced features for logged-in users):

from open_shield.api.fastapi import get_optional_user_context

@app.get("/search")
def search(ctx: UserContext | None = Depends(get_optional_user_context)):
    if ctx:
        return personalized_results(ctx.user.id)
    return public_results()

Development

# Install dependencies
uv sync

# Run tests
uv run pytest

# Lint and format
uv run ruff check .

# Type check
uv run mypy src/

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

open_shield_python-0.2.7.tar.gz (959.1 kB view details)

Uploaded Source

Built Distribution

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

open_shield_python-0.2.7-py3-none-any.whl (20.3 kB view details)

Uploaded Python 3

File details

Details for the file open_shield_python-0.2.7.tar.gz.

File metadata

  • Download URL: open_shield_python-0.2.7.tar.gz
  • Upload date:
  • Size: 959.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for open_shield_python-0.2.7.tar.gz
Algorithm Hash digest
SHA256 7e4e185dc8451f5cfe84ea94a3f27366892b5dc12b80f09a91f5bd3e0eea676e
MD5 549b6fcec1f144c8277e80d6be7943a0
BLAKE2b-256 81c0fe08ec0c68525c93a0719f0d67bccaf33404c2feb23ff104e19579f55906

See more details on using hashes here.

File details

Details for the file open_shield_python-0.2.7-py3-none-any.whl.

File metadata

  • Download URL: open_shield_python-0.2.7-py3-none-any.whl
  • Upload date:
  • Size: 20.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for open_shield_python-0.2.7-py3-none-any.whl
Algorithm Hash digest
SHA256 ac2ecd360e457f4ade602d07306a53797b796c8f34bdace03952236811a7fd32
MD5 61a58d484854d059ec327591c00ae230
BLAKE2b-256 be3afc30265e725fcd2676d1dd5f14c07cb1e819e70cf99b2cdbdbac76d51a01

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