Metadata-Version: 2.5
Name: obelisk-auth
Version: 1.0.0
Summary: Drop-in OIDC client for the Obelisk Gate identity platform — login, tokens, refresh, userinfo, and local ES256 token verification.
Project-URL: Homepage, https://obeliskgate.com
Author: VaultSpark Studios
License: Proprietary
License-File: LICENSE
License-File: NOTICE
Keywords: auth,es256,jwt,obelisk,oidc,openid-connect,pkce
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: cryptography>=42.0.0
Requires-Dist: pyjwt>=2.8.0
Provides-Extra: test
Requires-Dist: pytest>=7.0; extra == 'test'
Description-Content-Type: text/markdown

# obelisk-auth (Python)

A drop-in OIDC client for the **Obelisk Gate** identity platform
(`https://obeliskgate.com`). It is the Python counterpart of the JavaScript
`@obeliskgate/obelisk-auth` package and speaks the exact same contract: login +
tokens + refresh + userinfo + **local** ES256 token verification.

Obelisk is a conformant OpenID Connect provider:

- `GET /.well-known/openid-configuration` — discovery (RFC 8414)
- `GET /.well-known/jwks.json` — JWKS (ES256 / P-256 public keys)
- `GET /auth/authorize` — authorization-code flow, **PKCE S256 only**
- `POST /auth/token` — exchange `code`+`code_verifier` → `id_token` / `access_token` / `refresh_token`
- `GET /auth/userinfo` — Bearer-authenticated claims

Tokens are ES256 JWTs, so they can be verified **locally** against the cached
JWKS with no network round-trip per request — the property that makes Obelisk
useful as an agent-era auth plane.

## The agent-era angle

Every token is locally verifiable, and the **id_token** carries an `obelisk`
claim block that proves *how* the principal authenticated:

```json
"obelisk": {
  "v": "obelisk-claims-v1",
  "assurance": "passkey",
  "anchor": "0a1b2c3d...",
  "rating": { "...": "live issuer security posture" }
}
```

`assurance` distinguishes operator-proof factors (passkey) from weaker ones, and
`anchor` ties the issuance into Obelisk's tamper-evident receipt chain. Pull it
out of verified claims with `obelisk_block(claims)`.

## Install

```bash
pip install obelisk-auth
```

Dependencies are stdlib `urllib` for HTTP plus `PyJWT` and `cryptography` for
ES256 verification (no hand-rolled crypto).

## Login (copy-paste)

```python
from obelisk_auth import ObeliskAuth, obelisk_block

auth = ObeliskAuth(
    issuer="https://obeliskgate.com",
    client_id="rp-statvault",
    redirect_uri="https://statvault.org/auth/callback",
)

# 1) Start the login. Persist code_verifier + state + nonce in the user session,
#    then redirect the browser to the authorization URL.
login = auth.begin_login(scope="openid profile offline_access")
session["pkce"] = {
    "code_verifier": login.code_verifier,
    "state": login.state,
    "nonce": login.nonce,
}
redirect(login.authorization_url)

# 2) On the callback (e.g. /auth/callback?code=...&state=...):
#    First confirm the returned `state` matches what you stashed (CSRF defense).
assert request.args["state"] == session["pkce"]["state"]

result = auth.complete_login(
    code=request.args["code"],
    code_verifier=session["pkce"]["code_verifier"],
    nonce=session["pkce"]["nonce"],
)

print(result.claims["sub"])                 # the user's stable subject id
print(obelisk_block(result.claims))         # assurance + anchor + rating

access_token = result.tokens["access_token"]
refresh_token = result.tokens.get("refresh_token")  # present with offline_access
```

## Verify a token locally (no network per request)

```python
from obelisk_auth import ObeliskAuth, obelisk_block

auth = ObeliskAuth(
    issuer="https://obeliskgate.com",
    client_id="rp-statvault",
    redirect_uri="https://statvault.org/auth/callback",
)

# On a protected route, given a Bearer access token:
v = auth.verify_access_token(bearer_token)
if not v.ok:
    raise Unauthorized(v.reason)            # e.g. "expired", "issuer-mismatch"

print(v.claims["sub"])

# Gate capability on how the user proved themselves:
block = obelisk_block(v.claims)
if block and block.get("assurance") != "passkey":
    raise Forbidden("this action requires a passkey-proven session")
```

The first `verify_*` call fetches the JWKS once and caches it by `kid`; later
calls verify in-process. If a key rotates (unknown `kid`), the SDK refetches the
JWKS exactly once and retries.

## Refresh + userinfo

```python
rotated = auth.refresh(refresh_token)       # rotating refresh tokens
new_access = rotated["access_token"]

info = auth.get_userinfo(new_access)        # Bearer-authenticated userinfo
print(info["sub"])
```

> Reusing a superseded refresh token revokes the whole token family on the
> server (OAuth 2.0 BCP reuse detection); be prepared to force re-authentication.

## API

| Method | Description |
| --- | --- |
| `discover()` | Fetch + cache the discovery document. |
| `get_jwks(force=False)` | Fetch + cache the JWKS. |
| `begin_login(scope=...)` → `LoginStart` | PKCE S256 authorize URL + `code_verifier`/`state`/`nonce`. |
| `complete_login(code, code_verifier, nonce=...)` → `LoginResult` | Exchange + verify id_token. |
| `verify_id_token(id_token, nonce=...)` | Local ES256 verify (raises on failure). |
| `verify_access_token(token)` → `VerifyResult` | Local ES256 verify (never raises). |
| `refresh(refresh_token)` | Rotate a refresh token. |
| `get_userinfo(access_token)` | Bearer userinfo. |
| `obelisk_block(claims)` | Extract the verified `obelisk` assurance block. |

## Testing

```bash
pip install -e ".[test]"
pytest
```

The bundled tests are pure unit tests (no live server): PKCE-S256 challenge
correctness, authorization-URL construction, full login flow against a mocked
token endpoint, and round-trip ES256 sign/verify with a generated P-256 key.
