Python client library for the RCD Identity OIDC provider.
Handle the full OAuth 2.0 / OpenID Connect lifecycle programmatically -authorization URL generation, PKCE, code exchange, RS256 JWT verification, refresh token rotation, and automatic token management.
pip install git+https://github.com/RedCokeDevelopment/rcdid.py.gitYou can directly send LLM.txt to your LLM for a quick overview of the library and build your application fast.
Alternatively, you can follow the examples below:
from rcd_id import RCDIdentityClient
client = RCDIdentityClient(
issuer="https://auth.rcd.network",
client_id="rcd_your_client_id",
client_secret="your-client-secret",
)
# 1. Build authorization URL (user visits in browser)
req = client.authorization_url(
redirect_uri="http://localhost:8080/callback",
scope="openid email profile",
)
# 2. After callback with ?code=..., exchange for tokens
tokens = client.exchange_code(
code="authorization-code-from-callback",
redirect_uri="http://localhost:8080/callback",
)
# 3. Use the access token
user = client.userinfo(access_token=tokens.access_token)
print(user.email) # user@example.com
# 4. Refresh when needed
new_tokens = client.refresh_token()
print(new_tokens.access_token) # new access token
print(new_tokens.refresh_token) # rotated refresh tokenfrom rcd_id import RCDIdentityClient
client = RCDIdentityClient(
issuer="https://auth.rcd.network",
client_id="rcd_your_public_client",
client_auth_method="none",
use_pkce=True,
redirect_uri="http://localhost:8080/callback",
)
req = client.authorization_url()
print(f"Visit: {req.url}")
print(f"Save code_verifier: {req.code_verifier}")
# In your callback handler:
tokens = client.exchange_code(
code="authorization-code",
code_verifier=req.code_verifier,
)client.set_tokens(tokens)
# This auto-refreshes if the access token is expired:
user = client.userinfo() # no token arg needed
# Or get a valid token explicitly:
valid_token = client.get_valid_token()RCD Identity rotates refresh tokens. Each refresh_token() call invalidates the old token. Use the on_token_refresh callback to persist new tokens:
def save_tokens(tokens):
db.execute(
"UPDATE clients SET refresh_token = ? WHERE client_id = ?",
[tokens.refresh_token, "my-client"],
)
client = RCDIdentityClient(
issuer="https://auth.rcd.network",
client_id="rcd_abc",
client_secret="secret",
on_token_refresh=save_tokens,
)from rcd_id import AsyncRCDIdentityClient
async with AsyncRCDIdentityClient(
issuer="https://auth.rcd.network",
client_id="rcd_abc",
client_secret="secret",
redirect_uri="http://localhost:8080/callback",
) as client:
tokens = await client.exchange_code(code="...")
user = await client.userinfo()| Method | Description |
|---|---|
discover() |
Fetch and cache OIDC discovery document |
authorization_url(...) |
Build the authorization URL with PKCE support |
exchange_code(code, ...) |
Exchange authorization code for tokens |
refresh_token(...) |
Refresh access token (handles rotation) |
verify_access_token(token) |
Verify RS256 JWT against JWKS |
verify_id_token(token, nonce) |
Verify ID token and optionally check nonce |
userinfo(access_token) |
Fetch the UserInfo endpoint |
set_tokens(tokens) |
Store tokens for managed mode |
get_valid_token() |
Return valid token, auto-refreshing if expired |
clear_tokens() |
Clear stored tokens |
| Field | Type | Description |
|---|---|---|
access_token |
str |
The access token |
token_type |
str |
Always "Bearer" |
expires_in |
int |
Access token TTL (seconds) |
refresh_token |
str|None |
Refresh token (rotated) |
refresh_token_expires_in |
int|None |
Refresh token TTL |
scope |
str|None |
Granted scopes |
id_token |
str|None |
ID token (JWT) |
is_expired(leeway=30) |
bool |
Check if token is expired |
| Exception | Description |
|---|---|
ConfigurationError |
Invalid client configuration |
DiscoveryError |
Failed to fetch/parse OIDC discovery |
TokenError |
Token endpoint error (carries .error, .error_description) |
TokenVerificationError |
JWT verification failed |
InvalidTokenError |
Token expired or unavailable |
UserInfoError |
Userinfo endpoint error |
SessionError |
State/nonce mismatch |
Configure via client_auth_method:
| Method | Description |
|---|---|
client_secret_basic (default) |
Authorization: Basic <base64(client_id:secret)> |
client_secret_post |
Secret in POST body |
none |
Public client (requires use_pkce=True) |
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
pytest tests/
# Type check
mypy rcd_id/
# Lint
ruff check rcd_id/ tests/