A lightweight, configurable Identity Provider for development and testing
Project description
NanoIDP
A lightweight, configurable Identity Provider for development and testing.
Supports OAuth2/OIDC and SAML 2.0 protocols with a full-featured web UI for configuration.
Features
- OAuth2 / OIDC - Full OAuth2 implementation with Authorization Code, Password, Client Credentials, Refresh Token, and Device Authorization grants
- PKCE Support - Proof Key for Code Exchange (RFC 7636) with S256 and plain methods
- Token Management - Introspection (RFC 7662) and Revocation (RFC 7009) endpoints
- OIDC Logout - End Session endpoint for RP-initiated logout
- Device Flow - Device Authorization Grant (RFC 8628) for CLI/IoT applications
- SAML 2.0 - SSO and AttributeQuery endpoints with configurable signed assertions
- MCP Server - Model Context Protocol integration for Claude Code
- Web UI - Full configuration interface for users, clients, settings, and more
- YAML Configuration - File-based configuration, no database required
- Attribute-based Access Control - Flexible authority prefixes and claims mapping
- Audit Logging - Track all authentication events
- Docker Support - Ready to deploy with Docker/Docker Compose
Installation
From PyPI
pip install nanoidp
From GHCR (Docker Image)
docker pull ghcr.io/cdelmonte-zg/nanoidp:latest
From Source
git clone https://github.com/cdelmonte-zg/nanoidp.git
cd nanoidp
pip install .
For Development
git clone https://github.com/cdelmonte-zg/nanoidp.git
cd nanoidp
pip install -e ".[dev]"
Quick Start
Initialize Configuration (Recommended for pip install)
When installing via pip, use the init command to create a configuration directory:
# Create config in ./config (default)
python -m nanoidp init
# Or specify a custom path
python -m nanoidp init ./my-idp-config
# Then start with that config
python -m nanoidp --config ./my-idp-config
This creates:
users.yaml- User definitionssettings.yaml- OAuth/SAML settingskeys/- Directory for RSA keys (auto-generated on startup)
Interactive Wizard
For a guided setup:
python -m nanoidp wizard
The wizard guides you through:
- Server configuration (host, port, issuer)
- OAuth client setup
- Admin user creation
- Token settings
Run the Server
# Run with default config (./config)
python -m nanoidp
# With custom config directory
python -m nanoidp --config /path/to/config
# Using environment variable
NANOIDP_CONFIG_DIR=/path/to/config python -m nanoidp
# With options
python -m nanoidp --port 8080 --debug
Using Docker
docker-compose up -d
The server will be available at http://localhost:8000
You can also run the published GHCR image directly:
docker run --rm -p 8000:8000 \
-v $(pwd)/config:/app/config \
ghcr.io/cdelmonte-zg/nanoidp:latest
Web Interface
Access the admin UI at http://localhost:8000:
- Dashboard - Overview and quick stats
- Users - Create, edit, delete users
- OAuth Clients - Manage OAuth2 client credentials
- Settings - Configure IdP settings (issuer, audience, SAML)
- Keys & Certs - View and regenerate RSA keys
- Claims - Configure authority prefix mappings
- Audit Log - View and export authentication events
- Token Tester - Generate and inspect tokens
Configuration
Users (config/users.yaml)
users:
admin:
password: "admin"
email: "admin@example.org"
identity_class: "INTERNAL"
entitlements:
- "ADMIN_ACCESS"
- "USER_MANAGEMENT"
roles:
- "USER"
- "ADMIN"
tenant: "default"
source_acl:
- "ACL_READ"
- "ACL_WRITE"
default_user: "admin"
Settings (config/settings.yaml)
server:
host: "0.0.0.0"
port: 8000
oauth:
issuer: "http://localhost:8000"
audience: "my-app"
token_expiry_minutes: 60
clients:
- client_id: "demo-client"
client_secret: "demo-secret"
description: "Default demo client"
saml:
entity_id: "http://localhost:8000/saml"
sso_url: "http://localhost:8000/saml/sso"
default_acs_url: "http://localhost:8080/login/saml2/sso/nanoidp"
sign_responses: true # Set to false for testing unsigned SAML flows
authority_prefixes:
roles: "ROLE_"
identity_class: "IDENTITY_"
entitlements: "ENT_"
logging:
verbose_logging: true # Include usernames/client_ids in logs (default: true)
Logging Configuration
NanoIDP logs all authentication events to both the audit log (viewable in the Web UI) and standard output.
logging:
level: INFO # DEBUG, INFO, WARNING, ERROR, CRITICAL
log_token_requests: true # Log token endpoint requests
log_saml_requests: true # Log SAML endpoint requests
verbose_logging: true # Include usernames/client_ids in log messages
Verbose Logging (verbose_logging: true, default):
- Log messages include user and client identifiers for debugging
- Example:
[login] POST /token - success (user: admin) (client: demo-client)
Non-Verbose Logging (verbose_logging: false):
- Log messages omit sensitive identifiers
- Example:
[login] POST /token - success
Set verbose_logging: false if you're concerned about PII in log files, though for a dev tool this is typically not an issue.
API Endpoints
OAuth2 / OIDC
| Endpoint | Description |
|---|---|
GET /.well-known/openid-configuration |
OIDC Discovery |
GET /.well-known/jwks.json |
JSON Web Key Set |
GET /authorize |
Authorization endpoint (login page) |
POST /token |
Token endpoint |
GET/POST /userinfo |
UserInfo endpoint |
POST /introspect |
Token Introspection (RFC 7662) |
POST /revoke |
Token Revocation (RFC 7009) |
GET/POST /logout |
OIDC End Session / Logout |
POST /device_authorization |
Device Authorization (RFC 8628) |
GET/POST /device |
Device verification page |
SAML
| Endpoint | Description |
|---|---|
GET /saml/metadata |
IdP Metadata |
GET/POST /saml/sso |
Single Sign-On (supports both HTTP-POST and HTTP-Redirect bindings) |
POST /saml/attribute-query |
Attribute Query |
SAML Bindings
NanoIDP supports both standard SAML 2.0 bindings for the SSO endpoint:
| Binding | HTTP Method | SAMLRequest Encoding |
|---|---|---|
| HTTP-POST | POST | Base64 only (uncompressed) |
| HTTP-Redirect | GET | DEFLATE compressed + Base64 |
Both bindings are advertised in the SAML metadata (/saml/metadata).
Strict Binding Mode
By default, NanoIDP operates in lenient mode for developer convenience, accepting GET requests with uncompressed SAMLRequest data (non-compliant but useful for debugging).
To enforce strict SAML 2.0 binding compliance:
Via configuration file (settings.yaml):
saml:
strict_binding: true # Reject GET with uncompressed data
Via Web UI:
- Go to
http://localhost:8000/settings - In the SAML Settings section, toggle "Strict SAML Binding"
- Click Save Settings
| Mode | GET with uncompressed data | GET with DEFLATE | POST uncompressed |
|---|---|---|---|
| Lenient (default) | Accepted | Accepted | Accepted |
| Strict | Rejected (400) | Accepted | Accepted |
SAML Response Signing
By default, NanoIDP signs all SAML responses with an XML digital signature. You can disable signing for testing scenarios that require unsigned SAML flows:
Via configuration file (settings.yaml):
saml:
sign_responses: false # Disable SAML response signing
Via Web UI:
- Go to
http://localhost:8000/settings - In the SAML Settings section, toggle "Sign SAML Responses"
- Click Save Settings
When sign_responses: true (default), responses include:
<ds:Signature>element with RSA-SHA256 signature<ds:X509Certificate>with the IdP certificate
When sign_responses: false, responses are sent without any signature elements.
XML Canonicalization Algorithm
By default, NanoIDP uses Exclusive C14N for XML canonicalization, which is the standard for SAML signatures and compatible with most modern SAML implementations. You can configure the algorithm based on your SP requirements:
Via configuration file (settings.yaml):
saml:
c14n_algorithm: exc_c14n # Default: Exclusive C14N 1.0 (standard for SAML)
# c14n_algorithm: c14n # C14N 1.0
# c14n_algorithm: c14n11 # C14N 1.1
Via Web UI:
- Go to
http://localhost:8000/settings - In the SAML Settings section, select the Canonicalization Algorithm from the dropdown
- Click Save Settings
| Value | Algorithm | Use Case |
|---|---|---|
exc_c14n (default) |
Exclusive C14N 1.0 | Standard for SAML, handles namespace isolation |
c14n |
C14N 1.0 | Legacy SAML implementations |
c14n11 |
C14N 1.1 | Newer implementations |
Why Exclusive C14N is the default:
Exclusive C14N is recommended by the SAML 2.0 specification because it only includes namespaces actually used in the signed element. This is important when SPs extract the <Assertion> element from the <Response> to verify the signature independently. With standard C14N, the signature includes parent namespaces that break when the Assertion is extracted.
REST API
| Endpoint | Description |
|---|---|
GET /api/health |
Health check |
GET /api/users |
List users |
GET /api/users/{username} |
Get user details |
POST /api/users/{username}/token |
Generate token |
GET /api/audit |
Get audit log |
POST /api/config/reload |
Reload configuration |
POST /api/keys/rotate |
Rotate cryptographic keys |
GET /api/keys/info |
Get key information |
Token Request Examples
Password Grant
curl -X POST 'http://localhost:8000/token' \
-u 'demo-client:demo-secret' \
-d 'grant_type=password&username=admin&password=admin'
Client Credentials Grant
curl -X POST 'http://localhost:8000/token' \
-u 'demo-client:demo-secret' \
-d 'grant_type=client_credentials'
Refresh Token
curl -X POST 'http://localhost:8000/token' \
-u 'demo-client:demo-secret' \
-d 'grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN'
Device Authorization Flow
# 1. Request device code
curl -X POST 'http://localhost:8000/device_authorization' \
-u 'demo-client:demo-secret' \
-d 'scope=openid'
# Response:
# {
# "device_code": "...",
# "user_code": "ABCD1234",
# "verification_uri": "http://localhost:8000/device",
# "expires_in": 600
# }
# 2. User visits verification_uri and enters user_code
# 3. Poll for token
curl -X POST 'http://localhost:8000/token' \
-u 'demo-client:demo-secret' \
-d 'grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=YOUR_DEVICE_CODE'
Token Introspection
curl -X POST 'http://localhost:8000/introspect' \
-u 'demo-client:demo-secret' \
-d 'token=YOUR_ACCESS_TOKEN'
Token Revocation
curl -X POST 'http://localhost:8000/revoke' \
-u 'demo-client:demo-secret' \
-d 'token=YOUR_ACCESS_TOKEN'
JWT Token Structure
{
"iss": "http://localhost:8000",
"sub": "admin",
"aud": "my-app",
"iat": 1704100000,
"exp": 1704103600,
"roles": ["USER", "ADMIN"],
"tenant": "default",
"identity_class": "INTERNAL",
"entitlements": ["ADMIN_ACCESS", "USER_MANAGEMENT"],
"authorities": [
"ROLE_USER",
"ROLE_ADMIN",
"IDENTITY_INTERNAL",
"ENT_ADMIN_ACCESS",
"ENT_USER_MANAGEMENT",
"ACL_READ",
"ACL_WRITE"
]
}
Development
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run in development mode
python -m nanoidp --debug
End-to-End Test Agent
NanoIDP includes a comprehensive test agent that validates all functionality:
# Run against local server (default: http://localhost:8000)
python examples/test_agent.py
# Run against custom URL
python examples/test_agent.py --url http://localhost:9000
# Verbose output
python examples/test_agent.py --verbose
# JSON output
python examples/test_agent.py --json
The test agent covers:
- Core: Health check, OIDC discovery
- OAuth2/OIDC: All grant types, token introspection, revocation, logout
- SAML 2.0: Metadata, SSO (POST/Redirect bindings), Attribute Query, signing config
- Key Management: Key info, rotation, post-rotation token validation
- REST API: Users, config, audit log
MCP Server (Model Context Protocol)
NanoIDP includes an MCP server for integration with Claude Code and other MCP-compatible tools.
Available Tools
| Tool | Description |
|---|---|
list_users |
List all configured users |
get_user |
Get details of a specific user |
create_user |
Create a new user |
delete_user |
Delete a user |
generate_token |
Generate OAuth2 access token for a user |
decode_token |
Decode JWT token (without verification) |
verify_token |
Verify JWT token signature and expiration |
list_clients |
List OAuth clients |
get_client |
Get client details |
get_settings |
Get current IdP settings |
reload_config |
Reload configuration from files |
get_oidc_discovery |
Get OIDC discovery document |
get_jwks |
Get JSON Web Key Set |
Claude Code CLI Configuration
Add to your project's .claude/settings.json:
{
"mcpServers": {
"nanoidp": {
"command": "python",
"args": ["-m", "nanoidp.mcp_server"],
"env": {
"NANOIDP_CONFIG_DIR": "./config"
}
}
}
}
Or if NanoIDP is installed globally:
{
"mcpServers": {
"nanoidp": {
"command": "nanoidp-mcp",
"env": {
"NANOIDP_CONFIG_DIR": "/path/to/config"
}
}
}
}
Claude Desktop Configuration
Add to ~/.claude/claude_desktop_config.json:
{
"mcpServers": {
"nanoidp": {
"command": "nanoidp-mcp",
"env": {
"NANOIDP_CONFIG_DIR": "/path/to/nanoidp/config"
}
}
}
}
Running Standalone
# Run MCP server directly
python -m nanoidp.mcp_server
Security
This is a development/testing tool and should NOT be used in production environments.
Security Measures
While NanoIDP is designed for development, it still implements basic security protections:
| Protection | Description |
|---|---|
| XXE Prevention | XML parsing uses secure lxml configuration with entity expansion disabled |
| XSS Prevention | User-controlled values in SAML responses are HTML-escaped |
| JWT Signature Verification | All tokens are verified using RS256 signatures |
| PKCE Support | Prevents authorization code interception attacks |
Note: As a dev tool, NanoIDP intentionally allows:
- Open redirects (any
post_logout_redirect_uriaccepted) - Plaintext passwords in config files (convenience over security)
- Permissive CORS by default
These trade-offs prioritize developer convenience over production-grade security.
Security Profiles
NanoIDP supports two security profiles:
| Profile | Description |
|---|---|
dev (default) |
Maximum convenience for development: plaintext passwords, permissive CORS, no rate limiting |
stricter-dev |
Semi-hardened mode: bcrypt passwords, restricted CORS, rate limiting, debug mode blocked |
# Run with stricter-dev profile
python -m nanoidp --profile stricter-dev
Feature comparison:
| Feature | dev |
stricter-dev |
|---|---|---|
| Password storage | Plaintext | bcrypt hash |
| CORS | * (all origins) |
localhost only |
| Rate limiting | None | 10 req/min on /token |
| Debug mode | Allowed | Blocked |
Key Management
External Keys
You can use your own RSA keys instead of auto-generated ones:
# settings.yaml
jwt:
external_keys:
private_key: /path/to/private.pem
public_key: /path/to/public.pem
kid: "my-custom-key-id"
Key Rotation
NanoIDP supports key rotation with multiple keys in JWKS for seamless token validation during rotation:
# Rotate keys via API
curl -X POST http://localhost:8000/api/keys/rotate
# Get key information
curl http://localhost:8000/api/keys/info
The JWKS endpoint (/.well-known/jwks.json) returns both the active key and previous keys (up to max_previous_keys, default 2) for validation of tokens signed with older keys.
MCP Server Security
The MCP server exposes powerful administrative tools and should ONLY be used:
- Locally on developer machines
- In isolated development environments
- Never exposed to network access
Mutating tools (those that modify configuration):
create_user,update_user,delete_usercreate_client,update_client,delete_clientgenerate_token,update_settings,save_config
Admin Secret Protection
When NANOIDP_MCP_ADMIN_SECRET is set, mutating operations require the secret:
{
"mcpServers": {
"nanoidp": {
"command": "nanoidp-mcp",
"env": {
"NANOIDP_CONFIG_DIR": "./config",
"NANOIDP_MCP_ADMIN_SECRET": "your-secret-here"
}
}
}
}
Readonly Mode
To completely disable mutating tools (create, update, delete, generate):
# Via CLI flag
nanoidp-mcp --readonly
# Via environment variable
NANOIDP_MCP_READONLY=true nanoidp-mcp
In Claude Code settings:
{
"mcpServers": {
"nanoidp": {
"command": "nanoidp-mcp",
"args": ["--readonly"],
"env": {
"NANOIDP_CONFIG_DIR": "./config"
}
}
}
}
Use readonly mode when you only need introspection (listing users, decoding tokens, viewing settings) but want to prevent accidental modifications.
All MCP tool calls are logged to the audit log.
For detailed usage examples with Claude Code, see docs/MCP_WORKFLOW.md.
Environment Variables
| Variable | Description | Default |
|---|---|---|
NANOIDP_CONFIG_DIR |
Configuration directory | ./config |
NANOIDP_MCP_ADMIN_SECRET |
Secret required for mutating MCP operations | (none) |
NANOIDP_MCP_READONLY |
Disable mutating MCP tools when set to true |
false |
PORT |
Server port | 8000 |
Releasing
NanoIDP uses GitHub Actions for automated releases to both PyPI and GHCR.
Release Process
# 1. Update version in pyproject.toml
# 2. Update CHANGELOG.md
# 3. Commit changes
git add -A && git commit -m "Release v1.0.1"
# 4. Create and push tag
git tag v1.0.1
git push origin main --tags
The workflow automatically:
- Runs all tests
- Builds the package
- Publishes to TestPyPI
- Publishes to PyPI (only for non-prerelease tags)
- Builds and publishes Docker images to GHCR
Container tags are derived from the git tag (for example v1.0.1); the latest tag is only published for non-prerelease tags.
Pre-release Testing
For testing releases before publishing to PyPI:
# Create a pre-release tag (publishes to TestPyPI and GHCR)
git tag v1.0.1-rc1
git push origin v1.0.1-rc1
# Install from TestPyPI to verify
pip install -i https://test.pypi.org/simple/ nanoidp==1.0.1rc1
License
MIT License - see LICENSE for details.
Contributing
Contributions are welcome! Please read CONTRIBUTING.md for guidelines.
❤️ Support NanoIDP
NanoIDP is maintained as an open-source project.
If it helps you test OAuth2, OpenID Connect, or SAML flows, you can support its development here:
- 💖 GitHub Sponsors: https://github.com/sponsors/cdelmonte-zg
- ☕ Buy Me a Coffee: https://buymeacoffee.com/nanoidp
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nanoidp-1.3.2.tar.gz.
File metadata
- Download URL: nanoidp-1.3.2.tar.gz
- Upload date:
- Size: 78.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
92f72479aef30f8b641dbcffd5c8230caeb07ee4523be41fe2f242004fd4eed9
|
|
| MD5 |
a9df3a03dc3e0cb8e81cda4e189ec241
|
|
| BLAKE2b-256 |
146e787dce3656a75f463b8e0e37fc9e1dc9e6244890e10008f806be6cfbdce4
|
Provenance
The following attestation bundles were made for nanoidp-1.3.2.tar.gz:
Publisher:
publish.yml on cdelmonte-zg/nanoidp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nanoidp-1.3.2.tar.gz -
Subject digest:
92f72479aef30f8b641dbcffd5c8230caeb07ee4523be41fe2f242004fd4eed9 - Sigstore transparency entry: 1188390964
- Sigstore integration time:
-
Permalink:
cdelmonte-zg/nanoidp@7fda615f135f3de140599ccde59f06ce3b3c30fe -
Branch / Tag:
refs/tags/v1.3.2 - Owner: https://github.com/cdelmonte-zg
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7fda615f135f3de140599ccde59f06ce3b3c30fe -
Trigger Event:
push
-
Statement type:
File details
Details for the file nanoidp-1.3.2-py3-none-any.whl.
File metadata
- Download URL: nanoidp-1.3.2-py3-none-any.whl
- Upload date:
- Size: 98.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b90e4909ad399b95d14806e42efe5292c6b974950a98bb7074d2206095e79f58
|
|
| MD5 |
6d949572c971707ba764528ca7fc0d2f
|
|
| BLAKE2b-256 |
bfd6fbcd564897a838df39b3974d5c321b4f19024e07c504661f4d9d07a64285
|
Provenance
The following attestation bundles were made for nanoidp-1.3.2-py3-none-any.whl:
Publisher:
publish.yml on cdelmonte-zg/nanoidp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nanoidp-1.3.2-py3-none-any.whl -
Subject digest:
b90e4909ad399b95d14806e42efe5292c6b974950a98bb7074d2206095e79f58 - Sigstore transparency entry: 1188390973
- Sigstore integration time:
-
Permalink:
cdelmonte-zg/nanoidp@7fda615f135f3de140599ccde59f06ce3b3c30fe -
Branch / Tag:
refs/tags/v1.3.2 - Owner: https://github.com/cdelmonte-zg
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7fda615f135f3de140599ccde59f06ce3b3c30fe -
Trigger Event:
push
-
Statement type: