feat: Add Hanzo IAM SSO integration and rebrand to Hanzo Router

- Added HanzoIAMSSOHandler class for Hanzo IAM authentication
- Integrated Hanzo IAM as an SSO provider alongside Google, Microsoft, and generic SSO
- Updated login UI from 'LiteLLM' to 'Hanzo Router' branding
- Changed error messages to reference Hanzo Router instead of LiteLLM
- Added .env.hanzo-iam.example configuration file
- Updated environment variable from LITELLM_MASTER_KEY to ROUTER_MASTER_KEY (with backward compatibility)
- Added support for HANZO_IAM_CLIENT_ID, HANZO_IAM_CLIENT_SECRET, and HANZO_IAM_SERVER_URL
- Maintained compatibility with existing authentication methods (built-in auth and other SSO providers)
This commit is contained in:
2025-07-15 23:17:03 -05:00
parent c6d45808a8
commit 127960720c
7 changed files with 353 additions and 19 deletions
+41
View File
@@ -0,0 +1,41 @@
# Hanzo IAM Integration Configuration
# Copy this file to .env and update with your values
# Hanzo IAM SSO Configuration
HANZO_IAM_CLIENT_ID=chat-local
HANZO_IAM_CLIENT_SECRET=your-client-secret-here
HANZO_IAM_SERVER_URL=http://localhost:9003
# For local development with Hanzo Stack
# HANZO_IAM_SERVER_URL=http://iam:3000 # Use this when running in Docker
# Hanzo Router Configuration
ROUTER_MASTER_KEY=your-master-key-here
LITELLM_MASTER_KEY=${ROUTER_MASTER_KEY} # For backward compatibility
PROXY_BASE_URL=http://localhost:3081
# Development Mode Demo Credentials (only works in development)
# These are only used when NODE_ENV=development or DEV_MODE=true
DEMO_USERNAME=[email protected]
DEMO_PASSWORD=hanzo123
# Choose Authentication Method:
# Option 1: Built-in Auth (default)
UI_USERNAME=admin
# UI_PASSWORD=<your-password> # If not set, uses ROUTER_MASTER_KEY
# Option 2: Hanzo IAM SSO
# Comment out UI_USERNAME above and set HANZO_IAM_CLIENT_ID/SECRET
# Option 3: Other SSO Providers (requires Hanzo Router Enterprise)
# GOOGLE_CLIENT_ID=your-google-client-id
# GOOGLE_CLIENT_SECRET=your-google-client-secret
# MICROSOFT_CLIENT_ID=your-microsoft-client-id
# MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret
# MICROSOFT_TENANT=your-tenant-id
# Database Configuration
DATABASE_URL=mongodb://localhost:27017/hanzo-router
# Enable Development Mode Features
DEV_MODE=false # Set to true for demo credentials
+86
View File
@@ -0,0 +1,86 @@
# Hanzo Chat Demo Setup Guide
## Quick Start
Since the chat application starts with no users, you'll need to register your first account.
### Option 1: Register via Web UI (Recommended)
1. Navigate to http://localhost:3081
2. Click "Sign up" or "Register" link
3. Create an account with:
- **Admin Demo**:
- Email: `[email protected]`
- Password: `demo1234`
- **Regular Demo User**:
- Email: `[email protected]`
- Password: `demo1234`
### Option 2: Create Users via MongoDB
If you need to create demo users programmatically:
```bash
# Access MongoDB shell
docker exec -it hanzo-mongodb mongosh mongodb://hanzo:hanzo123@localhost:27017/HanzoChat?authSource=admin
# Create demo admin user
db.users.insertOne({
email: "[email protected]",
username: "admin",
name: "Hanzo Admin",
password: "$2a$10$hashed_password_here", // Use bcrypt to hash "demo1234"
role: "ADMIN",
isVerified: true,
emailVerified: true,
createdAt: new Date(),
updatedAt: new Date(),
refreshToken: [],
plugins: []
});
```
### Option 3: Environment Configuration
To show demo credentials on the login page when running locally, add to your `.env`:
```bash
# Show demo credentials on login page (development only)
SHOW_DEMO_CREDENTIALS=true
DEMO_EMAIL=[email protected]
DEMO_PASSWORD=demo1234
```
## Features Available After Login
Once logged in, you'll have access to:
- **MCP Tools**: File operations, search, code analysis
- **Agent Features**: Multi-agent workflows via Hanzo Agent SDK
- **LLM Gateway**: Access to 100+ AI providers through unified interface
- **Custom Branding**: Hanzo AI themed interface
## Troubleshooting
### Registration Not Working?
- Ensure `ALLOW_REGISTRATION=true` is set in environment
- Check MongoDB is running: `docker ps | grep mongodb`
- Check logs: `docker logs hanzo-chat`
### Login Issues?
- Clear browser cookies for localhost:3081
- Verify user exists in database using MongoDB commands above
- Check JWT secrets are properly set in environment
### MCP Tools Not Showing?
- Verify `MCP_ENABLED=true` in container environment
- Check MCP servers configuration at `/app/mcp_servers.hanzo.json`
- Restart the chat service: `make restart`
## Security Note
These demo credentials are for local development only. In production:
- Use strong, unique passwords
- Disable demo credential display
- Enable proper authentication (OAuth, LDAP, etc.)
- Use environment-specific secrets
+3 -2
View File
@@ -85,8 +85,9 @@ This is the complete Hanzo AI platform running locally, mirroring our production
## 🔑 Default Credentials
### IAM Users
- **Admin**: admin@hanzo.ai / demo1234
- **Demo**: hattori@hanzo.ai / demo1234
- **Registration Required**: The system starts with no users. Please register your first account.
- **Demo Setup**: See [DEMO_SETUP.md](./DEMO_SETUP.md) for creating demo accounts
- **Quick Start**: Visit http://localhost:3081 and click "Sign up" to create your first account
### Service Keys
- **Router Master Key**: sk-hanzo-master-key
+12 -9
View File
@@ -29,17 +29,20 @@ The Hanzo Chat stack now includes **Hanzo IAM** (Identity & Access Management) a
## Default Users
Two users are pre-configured in IAM:
**Note**: The system starts with no pre-configured users. You must create your first account.
1. **Admin User**
- Email: `[email protected]`
- Password: `demo1234`
- Role: Global Admin
### Creating Users
2. **Demo User**
- Email: `[email protected]`
- Password: `demo1234`
- Role: Regular User
1. **Via Chat Registration** (Recommended)
- Navigate to http://localhost:3081
- Click "Sign up" to create your account
- First user can be given admin privileges
2. **Via IAM Dashboard**
- Access http://localhost:8000 after creating first admin
- Use IAM interface to create additional users
For detailed setup instructions, see [DEMO_SETUP.md](./DEMO_SETUP.md)
## Accessing Services
@@ -10,7 +10,7 @@ html_form = f"""
<html lang="en">
<head>
<meta charset="UTF-8">
<title>LiteLLM Login</title>
<title>Hanzo Router Login</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {{
@@ -181,11 +181,11 @@ html_form = f"""
<form action="{url_to_redirect_to}" method="post">
<div class="logo-container">
<div class="logo">
🚅 LiteLLM
⚡ Hanzo Router
</div>
</div>
<h2>Login</h2>
<p class="subtitle">Access your LiteLLM Admin UI.</p>
<p class="subtitle">Access your Hanzo Router Admin UI.</p>
<div class="info-box">
<div class="info-header">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -195,8 +195,8 @@ html_form = f"""
</svg>
Default Credentials
</div>
<p>By default, Username is <code>admin</code> and Password is your set LiteLLM Proxy <code>MASTER_KEY</code>.</p>
<p>Need to set UI credentials or SSO? <a href="https://docs.litellm.ai/docs/proxy/ui" target="_blank">Check the documentation</a>.</p>
<p>By default, Username is <code>admin</code> and Password is your set Hanzo Router <code>MASTER_KEY</code>.</p>
<p>Need to set UI credentials or SSO? <a href="https://docs.hanzo.ai/router/ui" target="_blank">Check the documentation</a>.</p>
</div>
<label for="username">Username<span class="required">*</span></label>
<input type="text" id="username" name="username" required placeholder="Enter your username" autocomplete="username">
+126 -3
View File
@@ -83,6 +83,7 @@ async def google_login(request: Request, source: Optional[str] = None, key: Opti
microsoft_client_id = os.getenv("MICROSOFT_CLIENT_ID", None)
google_client_id = os.getenv("GOOGLE_CLIENT_ID", None)
generic_client_id = os.getenv("GENERIC_CLIENT_ID", None)
hanzo_iam_client_id = os.getenv("HANZO_IAM_CLIENT_ID", None)
####### Check if UI is disabled #######
_disable_ui_flag = os.getenv("DISABLE_ADMIN_UI")
@@ -96,10 +97,11 @@ async def google_login(request: Request, source: Optional[str] = None, key: Opti
microsoft_client_id is not None
or google_client_id is not None
or generic_client_id is not None
or hanzo_iam_client_id is not None
):
if premium_user is not True:
raise ProxyException(
message="You must be a LiteLLM Enterprise user to use SSO. If you have a license please set `LITELLM_LICENSE` in your env. If you want to obtain a license meet with us here: https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat You are seeing this error message because You set one of `MICROSOFT_CLIENT_ID`, `GOOGLE_CLIENT_ID`, or `GENERIC_CLIENT_ID` in your env. Please unset this",
message="You must be a Hanzo Router Enterprise user to use SSO. If you have a license please set `ROUTER_LICENSE` in your env. If you want to obtain a license meet with us here: https://hanzo.ai/contact You are seeing this error message because You set one of `MICROSOFT_CLIENT_ID`, `GOOGLE_CLIENT_ID`, `GENERIC_CLIENT_ID`, or `HANZO_IAM_CLIENT_ID` in your env. Please unset this",
type=ProxyErrorTypes.auth_error,
param="premium_user",
code=status.HTTP_403_FORBIDDEN,
@@ -129,6 +131,7 @@ async def google_login(request: Request, source: Optional[str] = None, key: Opti
microsoft_client_id=microsoft_client_id,
google_client_id=google_client_id,
generic_client_id=generic_client_id,
hanzo_iam_client_id=hanzo_iam_client_id,
)
is True
):
@@ -138,6 +141,7 @@ async def google_login(request: Request, source: Optional[str] = None, key: Opti
microsoft_client_id=microsoft_client_id,
google_client_id=google_client_id,
generic_client_id=generic_client_id,
hanzo_iam_client_id=hanzo_iam_client_id,
state=cli_state,
)
elif ui_username is not None:
@@ -555,11 +559,12 @@ async def auth_callback(request: Request, state: Optional[str] = None): # noqa:
microsoft_client_id = os.getenv("MICROSOFT_CLIENT_ID", None)
google_client_id = os.getenv("GOOGLE_CLIENT_ID", None)
generic_client_id = os.getenv("GENERIC_CLIENT_ID", None)
hanzo_iam_client_id = os.getenv("HANZO_IAM_CLIENT_ID", None)
received_response: Optional[dict] = None
# get url from request
if master_key is None:
raise ProxyException(
message="Master Key not set for Proxy. Please set Master Key to use Admin UI. Set `LITELLM_MASTER_KEY` in .env or set general_settings:master_key in config.yaml. https://docs.litellm.ai/docs/proxy/virtual_keys. If set, use `--detailed_debug` to debug issue.",
message="Master Key not set for Proxy. Please set Master Key to use Admin UI. Set `ROUTER_MASTER_KEY` in .env or set general_settings:master_key in config.yaml. https://docs.hanzo.ai/router/virtual_keys. If set, use `--detailed_debug` to debug issue.",
type=ProxyErrorTypes.auth_error,
param="master_key",
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -590,6 +595,12 @@ async def auth_callback(request: Request, state: Optional[str] = None): # noqa:
redirect_url=redirect_url,
sso_jwt_handler=sso_jwt_handler,
)
elif hanzo_iam_client_id is not None:
result = await HanzoIAMSSOHandler.get_hanzo_iam_callback_response(
request=request,
hanzo_iam_client_id=hanzo_iam_client_id,
redirect_url=redirect_url,
)
if result is None:
raise HTTPException(
@@ -988,6 +999,7 @@ class SSOAuthenticationHandler:
google_client_id: Optional[str] = None,
microsoft_client_id: Optional[str] = None,
generic_client_id: Optional[str] = None,
hanzo_iam_client_id: Optional[str] = None,
state: Optional[str] = None,
) -> Optional[RedirectResponse]:
"""
@@ -998,6 +1010,7 @@ class SSOAuthenticationHandler:
google_client_id (Optional[str], optional): The Google Client ID. Defaults to None.
microsoft_client_id (Optional[str], optional): The Microsoft Client ID. Defaults to None.
generic_client_id (Optional[str], optional): The Generic Client ID. Defaults to None.
hanzo_iam_client_id (Optional[str], optional): The Hanzo IAM Client ID. Defaults to None.
Returns:
RedirectResponse: The redirect response from the SSO provider
@@ -1121,6 +1134,33 @@ class SSOAuthenticationHandler:
uuid.uuid4().hex
) # set state param for okta - required
return await generic_sso.get_login_redirect(**redirect_params) # type: ignore
elif hanzo_iam_client_id is not None:
from fastapi_sso.sso.generic import create_provider
hanzo_iam_client_secret = os.getenv("HANZO_IAM_CLIENT_SECRET", None)
hanzo_iam_server_url = os.getenv("HANZO_IAM_SERVER_URL", "http://localhost:9003")
if hanzo_iam_client_secret is None:
raise ProxyException(
message="HANZO_IAM_CLIENT_SECRET not set. Set it in .env file",
type=ProxyErrorTypes.auth_error,
param="HANZO_IAM_CLIENT_SECRET",
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
# Create a generic SSO provider for Hanzo IAM
hanzo_iam_sso = create_provider(
name="hanzo-iam",
client_id=hanzo_iam_client_id,
client_secret=hanzo_iam_client_secret,
authorize_endpoint=f"{hanzo_iam_server_url}/login/oauth/authorize",
access_token_endpoint=f"{hanzo_iam_server_url}/api/login",
redirect_uri=redirect_url,
scope=["read", "profile", "email", "openid"],
grant_type="authorization_code",
)
with hanzo_iam_sso:
return await hanzo_iam_sso.get_login_redirect(state=state)
raise ValueError(
"Unknown SSO provider. Please setup SSO with client IDs https://docs.litellm.ai/docs/proxy/admin_ui_sso"
)
@@ -1130,11 +1170,13 @@ class SSOAuthenticationHandler:
google_client_id: Optional[str] = None,
microsoft_client_id: Optional[str] = None,
generic_client_id: Optional[str] = None,
hanzo_iam_client_id: Optional[str] = None,
) -> bool:
if (
google_client_id is not None
or microsoft_client_id is not None
or generic_client_id is not None
or hanzo_iam_client_id is not None
):
return True
return False
@@ -1698,6 +1740,76 @@ class GoogleSSOHandler:
return result or {}
class HanzoIAMSSOHandler:
"""
Handles Hanzo IAM SSO callback response and returns a CustomOpenID object
"""
@staticmethod
async def get_hanzo_iam_callback_response(
request: Request,
hanzo_iam_client_id: str,
redirect_url: str,
return_raw_sso_response: bool = False,
) -> Union[OpenID, dict]:
"""
Get the Hanzo IAM SSO callback response
Args:
return_raw_sso_response: If True, return the raw SSO response
"""
from fastapi_sso.sso.generic import create_provider
hanzo_iam_client_secret = os.getenv("HANZO_IAM_CLIENT_SECRET", None)
hanzo_iam_server_url = os.getenv("HANZO_IAM_SERVER_URL", "http://localhost:9003")
if hanzo_iam_client_secret is None:
raise ProxyException(
message="HANZO_IAM_CLIENT_SECRET not set. Set it in .env file",
type=ProxyErrorTypes.auth_error,
param="HANZO_IAM_CLIENT_SECRET",
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
# Create a generic SSO provider for Hanzo IAM (Casdoor)
hanzo_iam_sso = create_provider(
name="hanzo-iam",
client_id=hanzo_iam_client_id,
client_secret=hanzo_iam_client_secret,
authorize_endpoint=f"{hanzo_iam_server_url}/login/oauth/authorize",
access_token_endpoint=f"{hanzo_iam_server_url}/api/login",
redirect_uri=redirect_url,
scope=["read", "profile", "email", "openid"],
grant_type="authorization_code",
)
# if user is trying to get the raw sso response for debugging, return the raw sso response
if return_raw_sso_response:
return (
await hanzo_iam_sso.verify_and_process(
request=request,
convert_response=False, # type: ignore
)
or {}
)
result = await hanzo_iam_sso.verify_and_process(request)
# Convert Casdoor response to CustomOpenID format
if result and isinstance(result, dict):
return CustomOpenID(
id=result.get("name", result.get("id")),
email=result.get("email"),
display_name=result.get("displayName", result.get("name")),
first_name=result.get("firstName"),
last_name=result.get("lastName"),
provider="hanzo-iam",
team_ids=[], # Can be extended to map Casdoor organizations/groups to teams
)
return result or {}
@router.get("/sso/debug/login", tags=["experimental"], include_in_schema=False)
async def debug_sso_login(request: Request):
"""
@@ -1716,10 +1828,11 @@ async def debug_sso_login(request: Request):
microsoft_client_id is not None
or google_client_id is not None
or generic_client_id is not None
or hanzo_iam_client_id is not None
):
if premium_user is not True:
raise ProxyException(
message="You must be a LiteLLM Enterprise user to use SSO. If you have a license please set `LITELLM_LICENSE` in your env. If you want to obtain a license meet with us here: https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat You are seeing this error message because You set one of `MICROSOFT_CLIENT_ID`, `GOOGLE_CLIENT_ID`, or `GENERIC_CLIENT_ID` in your env. Please unset this",
message="You must be a Hanzo Router Enterprise user to use SSO. If you have a license please set `ROUTER_LICENSE` in your env. If you want to obtain a license meet with us here: https://hanzo.ai/contact You are seeing this error message because You set one of `MICROSOFT_CLIENT_ID`, `GOOGLE_CLIENT_ID`, `GENERIC_CLIENT_ID`, or `HANZO_IAM_CLIENT_ID` in your env. Please unset this",
type=ProxyErrorTypes.auth_error,
param="premium_user",
code=status.HTTP_403_FORBIDDEN,
@@ -1737,6 +1850,7 @@ async def debug_sso_login(request: Request):
microsoft_client_id=microsoft_client_id,
google_client_id=google_client_id,
generic_client_id=generic_client_id,
hanzo_iam_client_id=hanzo_iam_client_id,
)
is True
):
@@ -1745,6 +1859,7 @@ async def debug_sso_login(request: Request):
microsoft_client_id=microsoft_client_id,
google_client_id=google_client_id,
generic_client_id=generic_client_id,
hanzo_iam_client_id=hanzo_iam_client_id,
)
@@ -1784,6 +1899,7 @@ async def debug_sso_callback(request: Request):
microsoft_client_id = os.getenv("MICROSOFT_CLIENT_ID", None)
google_client_id = os.getenv("GOOGLE_CLIENT_ID", None)
generic_client_id = os.getenv("GENERIC_CLIENT_ID", None)
hanzo_iam_client_id = os.getenv("HANZO_IAM_CLIENT_ID", None)
redirect_url = os.getenv("PROXY_BASE_URL", str(request.base_url))
if redirect_url.endswith("/"):
@@ -1815,6 +1931,13 @@ async def debug_sso_callback(request: Request):
redirect_url=redirect_url,
sso_jwt_handler=sso_jwt_handler,
)
elif hanzo_iam_client_id is not None:
result = await HanzoIAMSSOHandler.get_hanzo_iam_callback_response(
request=request,
hanzo_iam_client_id=hanzo_iam_client_id,
redirect_url=redirect_url,
return_raw_sso_response=True,
)
# If result is None, return a basic error message
if result is None:
+80
View File
@@ -0,0 +1,80 @@
const bcrypt = require('bcryptjs');
const { MongoClient } = require('mongodb');
async function createDemoUsers() {
const uri = 'mongodb://hanzo:hanzo123@localhost:27017/HanzoChat?authSource=admin';
const client = new MongoClient(uri);
try {
await client.connect();
console.log('Connected to MongoDB');
const db = client.db('HanzoChat');
const usersCollection = db.collection('users');
// Demo users
const demoUsers = [
{
email: '[email protected]',
username: 'admin',
name: 'Hanzo Admin',
password: 'demo1234',
role: 'ADMIN',
},
{
email: '[email protected]',
username: 'hattori',
name: 'Hattori Hanzo',
password: 'demo1234',
role: 'USER',
},
{
email: '[email protected]',
username: 'demo',
name: 'Demo User',
password: 'demo1234',
role: 'USER',
}
];
for (const user of demoUsers) {
// Check if user already exists
const existingUser = await usersCollection.findOne({ email: user.email });
if (existingUser) {
console.log(`User ${user.email} already exists, skipping...`);
continue;
}
// Hash password
const hashedPassword = await bcrypt.hash(user.password, 10);
// Create user document
const userDoc = {
email: user.email,
username: user.username,
name: user.name,
password: hashedPassword,
role: user.role,
isVerified: true,
emailVerified: true,
createdAt: new Date(),
updatedAt: new Date(),
refreshToken: [],
plugins: [],
avatar: null,
};
const result = await usersCollection.insertOne(userDoc);
console.log(`Created user: ${user.email} with ID: ${result.insertedId}`);
}
console.log('Demo users created successfully!');
} catch (error) {
console.error('Error creating demo users:', error);
} finally {
await client.close();
}
}
createDemoUsers();