Skip to content

Commit 89699f2

Browse files
committed
Add standards-based Customer Identity OIDC client
1 parent 791568e commit 89699f2

6 files changed

Lines changed: 567 additions & 92 deletions

File tree

README.md

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ catches both surfaces without importing the one you do not use.
5151

5252
## Hosted Auth
5353

54-
Hosted Auth redirects the user to a branded NamoID sign-in page and returns a
55-
one-time code. The Client ID resolves the application, its environment, and its
56-
Hosted Auth domain, so there is no issuer or application UUID to configure.
54+
Hosted Auth uses standard OpenID Connect Authorization Code flow with S256 PKCE.
55+
The Client ID resolves the application and issuer; discovery supplies the
56+
authorization, token, UserInfo, revocation, JWKS, and logout endpoints.
5757

5858
```python
5959
from namoid import NamoIDClient
@@ -63,32 +63,32 @@ namoid = NamoIDClient(
6363
client_secret=os.environ["NAMOID_CLIENT_SECRET"], # server-side only
6464
)
6565

66-
# 1. Start a state-bound transaction and keep the verifier in the user's session.
67-
transaction = namoid.create_transaction()
66+
# 1. Start a state-, nonce-, and PKCE-bound transaction. Keep it server-side.
67+
transaction = namoid.create_oidc_transaction("https://app.example/auth/callback")
6868
session["namoid_state"] = transaction.state
69+
session["namoid_nonce"] = transaction.nonce
6970
session["namoid_verifier"] = transaction.code_verifier
7071

71-
# 2. Send the browser to the application's own hosted sign-in page.
72-
url = namoid.hosted_auth_url(
73-
return_to="https://app.example/auth/callback",
74-
state=transaction.state,
75-
completion_mode="confidential",
76-
code_challenge=transaction.code_challenge,
77-
)
72+
# 2. Send the browser to the discovered authorization endpoint.
73+
url = namoid.authorization_url(transaction)
7874

79-
# 3. On the callback, compare state, then exchange the code on the server.
75+
# 3. On the callback, compare state, then exchange using the same redirect URI.
8076
tokens = namoid.exchange_code(
8177
code=request.args["code"],
8278
code_verifier=session.pop("namoid_verifier"),
79+
redirect_uri="https://app.example/auth/callback",
8380
)
8481

85-
# 4. Confirm the token and create your own application session.
86-
result = namoid.validate_access_token(tokens.access_token)
87-
if not result.valid:
88-
raise Unauthorized()
82+
# 4. Verify the ID token signature and callback-bound nonce, then fetch UserInfo.
83+
claims = namoid.validate_id_token(tokens.raw["id_token"],
84+
nonce=session.pop("namoid_nonce"))
85+
user = namoid.user_info(tokens.access_token)
86+
assert claims["sub"] == user["sub"]
8987

90-
# 5. On sign-out, revoke the NamoID session too.
91-
namoid.revoke_session(access_token=tokens.access_token, refresh_token=tokens.refresh_token)
88+
# 5. On sign-out, revoke the refresh token and redirect through provider logout.
89+
namoid.revoke_token(tokens.refresh_token, token_type_hint="refresh_token")
90+
url = namoid.logout_url(id_token_hint=tokens.raw["id_token"],
91+
post_logout_redirect_uri="https://app.example/signed-out")
9292
```
9393

9494
`AsyncNamoIDClient` has exactly the same methods with `await`, for FastAPI,
@@ -98,25 +98,36 @@ Starlette, or any async framework:
9898
from namoid import AsyncNamoIDClient
9999

100100
async with AsyncNamoIDClient(client_id=..., client_secret=...) as namoid:
101-
tokens = await namoid.exchange_code(code=code, code_verifier=verifier)
101+
tokens = await namoid.exchange_code(
102+
code=code, code_verifier=verifier,
103+
redirect_uri="https://app.example/auth/callback",
104+
)
102105
```
103106

104107
Both accept an `http_client` if you want to supply your own configured
105108
`httpx.Client` / `httpx.AsyncClient`, and cache the auth config after the first
106109
fetch.
107110

108-
For a browser-only public client, redirect with `completion_mode="public"` and
109-
exchange with `confidential=False` — PKCE protects the flow and no secret is
110-
involved. Never put a Client Secret anywhere a browser can reach.
111+
For a public client, omit `client_secret`; PKCE protects the code exchange. For
112+
a confidential web application, the SDK sends the secret using HTTP Basic
113+
authentication at the discovered token endpoint. Never put a Client Secret
114+
anywhere a browser can reach.
111115

112116
| Method | Endpoint |
113117
|---|---|
114118
| `get_auth_config()` | `GET /v1/auth/config` |
115-
| `hosted_auth_url(...)` | builds the URL, no request |
116-
| `exchange_code(...)` | `POST /v1/auth/hosted/exchange` |
117-
| `refresh(...)` | `POST /v1/auth/refresh` |
119+
| `get_oidc_discovery()` | issuer `/.well-known/openid-configuration` |
120+
| `authorization_url(...)` | discovered authorization endpoint |
121+
| `exchange_code(...)` | discovered token endpoint |
122+
| `refresh(...)` | discovered token endpoint |
123+
| `user_info(...)` | discovered UserInfo endpoint |
124+
| `validate_id_token(...)` | discovered JWKS endpoint; local verification |
125+
| `revoke_token(...)` | discovered revocation endpoint |
126+
| `logout_url(...)` | discovered end-session endpoint |
118127
| `validate_access_token(...)` | `POST /v1/auth/tokens/validate` |
119-
| `revoke_session(...)` | `POST /v1/auth/logout` |
128+
129+
The older `hosted_auth_url(...)` and `revoke_session(...)` helpers remain for
130+
applications using NamoID's legacy Hosted Auth contract.
120131

121132
Every failure raises `NamoIDError`, carrying `status`, `code` (the API's own
122133
error code when present), and the parsed `detail`.

pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "namoid"
7-
version = "0.1.0"
7+
version = "0.2.0"
88
description = "Python SDK for NamoID, enterprise identity for India (OAuth 2.1 / OIDC)."
99
readme = "README.md"
1010
requires-python = ">=3.10"
@@ -44,18 +44,17 @@ classifiers = [
4444

4545
dependencies = [
4646
"httpx>=0.28",
47+
"joserfc>=1.0",
4748
]
4849

4950
[project.optional-dependencies]
5051
# Protect an MCP server. Framework-agnostic core: discovery, audience-bound
5152
# token verification, and RFC 9728 metadata.
5253
mcp = [
53-
"joserfc>=1.0",
5454
]
5555
# The same core wired into FastMCP. `fastmcp` itself requires a newer Python
5656
# than this package's floor, so pip enforces that when the extra is installed.
5757
fastmcp = [
58-
"joserfc>=1.0",
5958
"fastmcp>=3.4.5,<4",
6059
]
6160

src/namoid/__init__.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from importlib import import_module
2424
from typing import TYPE_CHECKING, Any
2525

26-
__version__ = "0.1.0"
26+
__version__ = "0.2.0"
2727
__homepage__ = "https://namoid.in"
2828

2929
# Public name -> the module that defines it. Resolved on first attribute access
@@ -37,6 +37,12 @@
3737
"HostedAuthTransaction": "namoid.hosted_auth",
3838
"TokenResponse": "namoid.hosted_auth",
3939
"TokenValidation": "namoid.hosted_auth",
40+
"OIDCDiscovery": "namoid.oidc",
41+
"OIDCTransaction": "namoid.oidc",
42+
"build_authorization_url": "namoid.oidc",
43+
"build_logout_url": "namoid.oidc",
44+
"create_oidc_transaction": "namoid.oidc",
45+
"validate_id_token": "namoid.oidc",
4046
"build_configured_hosted_auth_url": "namoid.hosted_auth",
4147
"build_hosted_auth_url": "namoid.hosted_auth",
4248
"create_hosted_auth_transaction": "namoid.hosted_auth",
@@ -48,12 +54,18 @@
4854
"HostedAuthTransaction",
4955
"NamoIDClient",
5056
"NamoIDError",
57+
"OIDCDiscovery",
58+
"OIDCTransaction",
5159
"TokenResponse",
5260
"TokenValidation",
5361
"__homepage__",
5462
"__version__",
5563
"build_configured_hosted_auth_url",
5664
"build_hosted_auth_url",
65+
"build_authorization_url",
66+
"build_logout_url",
67+
"create_oidc_transaction",
68+
"validate_id_token",
5769
"create_hosted_auth_transaction",
5870
]
5971

@@ -84,6 +96,12 @@ def __dir__() -> list[str]:
8496
from namoid.hosted_auth import (
8597
build_configured_hosted_auth_url as build_configured_hosted_auth_url,
8698
)
99+
from namoid.oidc import OIDCDiscovery as OIDCDiscovery
100+
from namoid.oidc import OIDCTransaction as OIDCTransaction
101+
from namoid.oidc import build_authorization_url as build_authorization_url
102+
from namoid.oidc import build_logout_url as build_logout_url
103+
from namoid.oidc import create_oidc_transaction as create_oidc_transaction
104+
from namoid.oidc import validate_id_token as validate_id_token
87105
from namoid.hosted_auth import build_hosted_auth_url as build_hosted_auth_url
88106
from namoid.hosted_auth import (
89107
create_hosted_auth_transaction as create_hosted_auth_transaction,

0 commit comments

Comments
 (0)