Metadata-Version: 2.4
Name: azure-sso
Version: 0.1.0
Summary: Reusable Azure AD (Entra ID) OIDC + local break-glass login library with RBAC and audit logging. Framework-agnostic core with Flask and FastAPI adapters.
Author: DOES / dboyd
License: Proprietary
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: msal>=1.31
Provides-Extra: postgres
Requires-Dist: psycopg2-binary>=2.9; extra == "postgres"
Provides-Extra: flask
Requires-Dist: Flask>=3.0; extra == "flask"
Requires-Dist: Flask-Login>=0.6; extra == "flask"
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.110; extra == "fastapi"
Requires-Dist: itsdangerous>=2.0; extra == "fastapi"
Requires-Dist: python-multipart>=0.0.9; extra == "fastapi"
Provides-Extra: flask-all
Requires-Dist: Flask>=3.0; extra == "flask-all"
Requires-Dist: Flask-Login>=0.6; extra == "flask-all"
Requires-Dist: psycopg2-binary>=2.9; extra == "flask-all"
Provides-Extra: fastapi-all
Requires-Dist: fastapi>=0.110; extra == "fastapi-all"
Requires-Dist: uvicorn>=0.29; extra == "fastapi-all"
Requires-Dist: itsdangerous>=2.0; extra == "fastapi-all"
Requires-Dist: python-multipart>=0.0.9; extra == "fastapi-all"
Requires-Dist: psycopg2-binary>=2.9; extra == "fastapi-all"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"

# azure-sso (`azsso`)

A small, reusable Python **user-login library** for DOES web apps. It packages the
three login methods proven in the [does-catalog](../does-catalog) blueprint so any
app — starting with the **addverify GUI** — can wrap them cleanly instead of
re-implementing auth each time.

| Login method | What it is |
|---|---|
| **Azure AD (Entra ID) OIDC** | MSAL confidential-client auth-code flow. Auto-provision on first sign-in, or pre-provision a role beforehand. |
| **Local break-glass** | Username + password (PBKDF2-SHA256), flagged in the audit log. Works when AD is unavailable or not yet configured. |
| **RBAC + audit** | Configurable role hierarchy (`viewer < analyst < admin` by default) and an append-only `audit_log` covering every auth and admin action. |

## Design

```
              ┌──────────────────────────────┐
              │        AuthService           │  ← framework-agnostic core
              │  (all login + admin logic)   │    imports NO web framework
              └───────────────┬──────────────┘
        ┌─────────────────────┼─────────────────────┐
   ┌────┴─────┐        ┌───────┴───────┐      ┌───────┴────────┐
   │ MsalAuth │        │   UserStore   │      │   security     │
   │ (OIDC)   │        │  (pluggable)  │      │ (pbkdf2 hash)  │
   └──────────┘        └───────┬───────┘      └────────────────┘
                    ┌──────────┴──────────┐
              ┌─────┴──────┐       ┌───────┴────────┐
              │  SQLite    │       │  PostgreSQL    │
              └────────────┘       └────────────────┘

   Adapters (thin shells over AuthService):
   • azsso.adapters.flask    — blueprint + flask-login (matches does-catalog UX)
   • azsso.adapters.fastapi  — router + session auth
```

Two design choices you asked for:

- **Both frameworks.** The login logic lives once in `AuthService`; Flask and
  FastAPI are thin adapters. No duplicated auth code.
- **Its own, movable database.** The user/audit store has a dedicated
  `AUTH_DB_URL`, separate from the host app's main DB. Point it at a standalone
  database on today's Postgres instance; move it to another server later by
  changing one env var — no code change. SQLite is the zero-infra dev default.

## Install

```bash
pip install -e '.[flask-all]'      # Flask + flask-login + psycopg2
pip install -e '.[fastapi-all]'    # FastAPI + uvicorn + psycopg2
pip install -e '.[dev]'            # tests only (SQLite, no web framework)
```

## Configure

Copy `.env.example` → `.env`. Minimum for local dev:

```bash
SECRET_KEY=some-long-random-string
AUTH_DB_URL=sqlite:///azsso_auth.db
```

Add the four `AZURE_*` vars to light up "Sign in with Microsoft"; leave them out
and that button is hidden while local login keeps working.

## Use it — Flask

```python
from flask import Flask
from flask_login import login_required, current_user
from azsso.adapters.flask import init_auth, require_role

app = Flask(__name__)
app.secret_key = ...
init_auth(app, url_prefix="/auth")      # registers all /auth/* routes

@app.route("/")
@login_required
def index():
    return f"hi {current_user.username}"

@app.route("/admin")
@require_role("admin")
def admin():
    ...
```

Registers: `/auth/login`, `/auth/login/ad`, `/auth/login/callback`,
`/auth/login/local`, `/auth/logout`, and the `/auth/admin/users` management
screens. Bundled default templates render out of the box; define your own
`login.html` / `admin_users.html` / `admin_user_edit.html` to override.

## Use it — FastAPI

```python
from fastapi import FastAPI, Depends
from azsso.adapters.fastapi import init_auth, require_role
from azsso.models import User

app = FastAPI()
init_auth(app, prefix="/auth", secret_key=...)

@app.get("/")
def index(user: User = Depends(require_role("viewer"))):
    return {"me": user.username}
```

## Use it — no framework

```python
from azsso import AuthService
svc = AuthService()                              # config + store from env
svc.create_local_user("admin", "Admin", "admin", actor="bootstrap")  # -> temp pw
svc.local_login("admin", "<temp pw>")            # -> LoginResult
svc.provision_ad_user("jane@dc.gov", "Jane", "analyst", actor="admin")
```

## Bootstrap the first admin

There is no seeded account (by design). Create one once:

```bash
python -c "from azsso import AuthService; \
  print(AuthService().create_local_user('admin','Admin','admin',actor='bootstrap').temp_password)"
```

Copy the printed temporary password, sign in at `/auth/login`, then create AD or
additional accounts from the Users screen.

## Azure AD app registration

**Full walkthrough: [`docs/azure-setup.md`](docs/azure-setup.md)** — per-app
registration steps, the exact permissions required (runtime vs. setup-time),
local-dev redirects, first-admin bootstrap, and troubleshooting.

Quick version:

```bash
az ad app create --display-name "addverify-webapp" \
  --sign-in-audience AzureADMyOrg \
  --web-redirect-uris "https://addverify.doesworks.net/auth/login/callback"
az ad app credential reset --id <appId> --display-name "addverify-secret"
```

Tenant for all DC.gov apps: `8fe449f1-8b94-4fb7-9906-6f939da82d73`. No Graph API
permissions or admin consent are needed (see the doc's Permissions section).

## Test

```bash
pip install -e '.[dev]' && python -m pytest -q
```

Core tests run on SQLite with no web framework; adapter tests skip gracefully if
Flask/FastAPI aren't installed.

## Schema

`users` and `audit_log` match the does-catalog columns, so a Postgres store can
live beside the existing catalog schema in its own database.

| `users` | `audit_log` |
|---|---|
| id, username (unique), email, display_name, password_hash, auth_source (`ad`/`local`), role, is_active, is_break_glass, created_by, created_at, last_login | id, action, changed_by, ip_address, reason, target, created_at |
