diff --git a/products-web/.env.keycloak.example b/products-web/.env.keycloak.example new file mode 100644 index 0000000..eea9454 --- /dev/null +++ b/products-web/.env.keycloak.example @@ -0,0 +1,37 @@ +# Keycloak OAuth Configuration Example +# Copy this file to .env and fill in your actual Keycloak values + +# Keycloak Client Configuration +CLIENT_ID=products-web-client +CLIENT_SECRET=your_keycloak_client_secret_here + +# Keycloak Server Configuration +# Replace keycloak.example.com and realm-name with your actual values +KEYCLOAK_SERVER_URL=https://keycloak.example.com +KEYCLOAK_REALM=your-realm-name + +# OAuth Scopes (space-separated) +# Keycloak common scopes: openid profile email +SCOPE=openid profile email + +# Redirect URI (must match what's configured in Keycloak client) +REDIRECT_URI=http://localhost:8501/oauth2callback + +# Base URL will be constructed automatically from KEYCLOAK_SERVER_URL and KEYCLOAK_REALM +# Leave empty to auto-construct, or set manually for custom configurations +BASE_URL= + +# For manual URL configuration (advanced users only - leave empty for auto-construction): +# AUTHORIZE_URL=https://keycloak.example.com/realms/your-realm-name/protocol/openid-connect/auth +# TOKEN_URL=https://keycloak.example.com/realms/your-realm-name/protocol/openid-connect/token +# REFRESH_TOKEN_URL=https://keycloak.example.com/realms/your-realm-name/protocol/openid-connect/token +# REVOKE_TOKEN_URL=https://keycloak.example.com/realms/your-realm-name/protocol/openid-connect/revoke + +# ProductsAgent API Configuration +PRODUCTS_AGENT_URL=http://localhost:8000 + +# Authentication Provider (set to 'keycloak' to enable Keycloak-specific features) +AUTH_PROVIDER=keycloak + +# Logging Level (DEBUG, INFO, WARNING, ERROR) +LOG_LEVEL=INFO \ No newline at end of file diff --git a/products-web/KEYCLOAK.md b/products-web/KEYCLOAK.md new file mode 100644 index 0000000..80c5d55 --- /dev/null +++ b/products-web/KEYCLOAK.md @@ -0,0 +1,264 @@ +# Keycloak Setup Guide for Products Web App + +This guide explains how to configure the products-web application to use Keycloak for authentication instead of Microsoft Entra ID. + +## Overview + +The products-web application supports both Microsoft Entra ID and Keycloak as OAuth2 authentication providers. This flexibility allows you to integrate with your existing identity management infrastructure. + +## Prerequisites + +- Keycloak server (version 15+ recommended) +- Administrative access to your Keycloak realm +- Products-web application source code +- Python 3.12+ with required dependencies + +## Keycloak Configuration + +### 1. Create a New Client + +1. Log into your Keycloak Admin Console +2. Navigate to your realm (or create a new one) +3. Go to **Clients** → **Create** +4. Configure the client: + +```yaml +Client ID: products-web-client +Client Protocol: openid-connect +Root URL: http://localhost:8501 +``` + +### 2. Client Settings + +Configure the following settings for your client: + +**Access Type:** `confidential` + +**Standard Flow:** `Enabled` (Authorization Code Flow) + +**Valid Redirect URIs:** +``` +http://localhost:8501/oauth2callback +https://your-domain.com/oauth2callback # For production +``` + +**Web Origins:** +``` +http://localhost:8501 +https://your-domain.com # For production +``` + +**Advanced Settings:** +- Proof Key for Code Exchange Code Challenge Method: `S256` (recommended) + +### 3. Get Client Credentials + +1. Go to the **Credentials** tab of your client +2. Copy the **Secret** value - you'll need this for `CLIENT_SECRET` + +### 4. Configure User Attributes (Optional) + +If you want to include additional user information in JWT tokens: + +1. Go to **Client Scopes** +2. Add desired scopes like `profile`, `email` +3. Configure mappers for additional claims if needed + +## Application Configuration + +### 1. Environment Variables + +Create a `.env` file based on `.env.keycloak.example`: + +```env +# Keycloak Client Configuration +CLIENT_ID=products-web-client +CLIENT_SECRET=your_actual_client_secret_here + +# Keycloak Server Configuration +KEYCLOAK_SERVER_URL=https://keycloak.yourdomain.com +KEYCLOAK_REALM=your-realm-name + +# OAuth Scopes +SCOPE=openid profile email + +# Redirect URI +REDIRECT_URI=http://localhost:8501/oauth2callback + +# Authentication Provider +AUTH_PROVIDER=keycloak + +# ProductsAgent API Configuration +PRODUCTS_AGENT_URL=http://localhost:8000 + +# Logging +LOG_LEVEL=INFO +``` + +### 2. Alternative: Manual URL Configuration + +For advanced users who want to specify URLs manually: + +```env +# Manual URL configuration (instead of auto-construction) +AUTH_PROVIDER=keycloak +AUTHORIZE_URL=https://keycloak.yourdomain.com/realms/your-realm/protocol/openid-connect/auth +TOKEN_URL=https://keycloak.yourdomain.com/realms/your-realm/protocol/openid-connect/token +REFRESH_TOKEN_URL=https://keycloak.yourdomain.com/realms/your-realm/protocol/openid-connect/token +REVOKE_TOKEN_URL=https://keycloak.yourdomain.com/realms/your-realm/protocol/openid-connect/revoke +``` + +## Running the Application + +### 1. Install Dependencies + +```bash +cd products-web +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +### 2. Set Environment Variables + +```bash +# Copy and modify the Keycloak example +cp .env.keycloak.example .env + +# Edit .env with your actual Keycloak configuration +nano .env +``` + +### 3. Start the Application + +```bash +streamlit run app.py +``` + +### 4. Test Authentication + +1. Navigate to `http://localhost:8501` +2. Click "Login with Keycloak" +3. You'll be redirected to your Keycloak login page +4. After successful authentication, you'll be returned to the application + +## Troubleshooting + +### Common Issues + +#### 1. "Invalid redirect URI" +- Ensure the redirect URI in your `.env` exactly matches what's configured in Keycloak +- Check for trailing slashes and protocol mismatches + +#### 2. "Client not found" +- Verify `CLIENT_ID` matches the client ID in Keycloak +- Ensure you're using the correct realm + +#### 3. "Invalid client credentials" +- Double-check the `CLIENT_SECRET` value +- Ensure the client is set to "confidential" access type + +#### 4. "Invalid scope" +- Verify the scopes in your `.env` are configured in Keycloak +- Common scopes: `openid`, `profile`, `email` + +### Debugging Tips + +#### Enable Debug Logging +```env +LOG_LEVEL=DEBUG +``` + +#### Check Configuration Display +The application shows configuration details in the "Configuration Details" expander on the authentication page. + +#### Network Debugging +Use browser developer tools to inspect: +- OAuth redirect flows +- Token responses +- API calls to Keycloak endpoints + +## Advanced Configuration + +### Custom Claims and Scopes + +To include custom user attributes in tokens: + +1. **Create Custom Client Scope** in Keycloak +2. **Add Protocol Mappers** for your attributes +3. **Assign Scope** to your client +4. **Update SCOPE** environment variable + +Example for custom user attributes: +```env +SCOPE=openid profile email roles groups custom-attributes +``` + +### SSL/TLS Configuration + +For production deployments: + +1. **Use HTTPS URLs** in all configuration +2. **Configure SSL certificates** in Keycloak +3. **Update redirect URIs** to use HTTPS +4. **Set secure cookie options** if needed + +### Multi-Realm Support + +To support multiple Keycloak realms: + +1. Set up separate `.env` files per realm +2. Use different client IDs for each realm +3. Consider using realm-specific redirect URIs + +## Security Considerations + +### Production Checklist + +- [ ] Use HTTPS for all URLs +- [ ] Set strong client secrets +- [ ] Configure appropriate token lifetimes +- [ ] Enable PKCE (Proof Key for Code Exchange) +- [ ] Review and minimize scope permissions +- [ ] Set up proper CORS policies +- [ ] Configure session timeout appropriately +- [ ] Enable audit logging in Keycloak + +### Token Security + +- Tokens are stored in Streamlit session state (server-side) +- JWT validation occurs on every API call +- Automatic token refresh is supported +- Logout clears all session data + +## Comparison with Entra ID + +| Feature | Keycloak | Microsoft Entra ID | +|---------|----------|-------------------| +| **Cost** | Free (self-hosted) | Paid per user | +| **Deployment** | Self-managed | Cloud-managed | +| **Customization** | Highly customizable | Limited customization | +| **Enterprise Features** | Full-featured | Advanced enterprise features | +| **Setup Complexity** | Medium | Low | +| **Token Standards** | Full OAuth2/OIDC | Full OAuth2/OIDC | + +## Support and Resources + +### Keycloak Documentation +- [Keycloak Documentation](https://www.keycloak.org/documentation) +- [OAuth2/OIDC Configuration](https://www.keycloak.org/docs/latest/server_admin/index.html#_client_credentials) + +### Application Resources +- Check the main README.md for overall architecture +- See QUICKSTART.md for quick setup instructions +- Review products-agent README for backend configuration + +### Getting Help + +If you encounter issues: + +1. Check the application logs (`LOG_LEVEL=DEBUG`) +2. Verify Keycloak server logs +3. Review network traffic in browser dev tools +4. Consult Keycloak community forums +5. Check the OAuth2 specification for protocol details \ No newline at end of file diff --git a/products-web/QUICKSTART.md b/products-web/QUICKSTART.md index d3f8a93..70cd9ea 100644 --- a/products-web/QUICKSTART.md +++ b/products-web/QUICKSTART.md @@ -12,15 +12,32 @@ source .venv/bin/activate uv pip install streamlit streamlit-oauth "python-jose[cryptography]" python-dotenv requests cryptography ``` -### 2. Configure Environment +### 2. Configure Authentication Provider + +Choose your preferred authentication provider: + +#### Option A: Microsoft Entra ID (Default) ```bash -# Copy environment template +# Copy environment template cp .env.example .env # Edit .env with your Microsoft Entra ID credentials: # CLIENT_ID, CLIENT_SECRET, TENANT_ID ``` +#### Option B: Keycloak +```bash +# Copy Keycloak environment template +cp .env.keycloak.example .env + +# Edit .env with your Keycloak configuration: +# CLIENT_ID, CLIENT_SECRET, KEYCLOAK_SERVER_URL, KEYCLOAK_REALM +``` + +📖 **For detailed Keycloak setup, see [KEYCLOAK.md](./KEYCLOAK.md)** +# CLIENT_ID, CLIENT_SECRET, TENANT_ID +``` + ### 3. Run Application ```bash # Start the Streamlit app diff --git a/products-web/README.md b/products-web/README.md index f39da2e..cd7a188 100644 --- a/products-web/README.md +++ b/products-web/README.md @@ -1,14 +1,26 @@ # Products Web - Streamlit Application -A modern Streamlit web application that provides a secure interface for Microsoft Entra ID OAuth authentication integrated with an intelligent ProductsAgent chat system. This application serves as the frontend for interacting with AI-powered product management capabilities while demonstrating end-to-end authentication flows in a zero-trust architecture. +A modern Streamlit web application that provides a secure interface for OAuth authentication integrated with an intelligent ProductsAgent chat system. This application serves as the frontend for interacting with AI-powered product management capabilities while demonstrating end-to-end authentication flows in a zero-trust architecture. +## Supported Authentication Providers + +This application supports multiple OAuth2/OpenID Connect providers: + +- **Microsoft Entra ID** (Azure AD) - Default provider +- **Keycloak** - Open-source identity and access management +- **Custom OAuth2/OIDC** - Any compliant OAuth2 provider + +📖 **For Keycloak setup instructions, see [KEYCLOAK.md](./KEYCLOAK.md)** ## Prerequisites - Python 3.12 or higher - [uv](https://github.com/astral-sh/uv) package manager - Docker and Docker Compose (for containerized deployment) -- Microsoft Entra ID tenant with configured application registration +- Authentication provider: + - **Microsoft Entra ID**: tenant with configured application registration, OR + - **Keycloak**: server with configured client, OR + - **Custom OAuth2**: provider with client credentials ## Local Development Setup @@ -42,7 +54,11 @@ uv pip install -r requirements.txt uv pip install -e . ``` -### 3. Environment Configuration +### 3. Authentication Provider Configuration + +Choose one of the supported authentication providers and create the appropriate configuration: + +#### Option A: Microsoft Entra ID (Default) Create a `.env` file with your Microsoft Entra ID configuration: @@ -57,6 +73,61 @@ SCOPE=openid profile email api://.onmicrosoft.com/products-agent/Age REDIRECT_URI=http://localhost:8501/oauth2callback BASE_URL=https://login.microsoftonline.com +# Authentication Provider (optional, defaults to entra_id) +AUTH_PROVIDER=entra_id + +# ProductsAgent API Configuration +PRODUCTS_AGENT_URL=http://localhost:8001 +``` + +#### Option B: Keycloak + +Create a `.env` file with your Keycloak configuration: + +```env +# Keycloak Client Configuration +CLIENT_ID=products-web-client +CLIENT_SECRET=your_keycloak_client_secret_here + +# Keycloak Server Configuration +KEYCLOAK_SERVER_URL=https://keycloak.yourdomain.com +KEYCLOAK_REALM=your-realm-name + +# OAuth Settings +SCOPE=openid profile email +REDIRECT_URI=http://localhost:8501/oauth2callback + +# Authentication Provider +AUTH_PROVIDER=keycloak + +# ProductsAgent API Configuration +PRODUCTS_AGENT_URL=http://localhost:8001 +``` + +📖 **For detailed Keycloak setup instructions, see [KEYCLOAK.md](./KEYCLOAK.md)** + +#### Option C: Custom OAuth2 Provider + +For other OAuth2/OIDC compliant providers, manually specify the endpoints: + +```env +# Custom OAuth2 Provider Configuration +CLIENT_ID=your_client_id +CLIENT_SECRET=your_client_secret + +# Manual URL Configuration +AUTHORIZE_URL=https://your-provider.com/oauth2/authorize +TOKEN_URL=https://your-provider.com/oauth2/token +REFRESH_TOKEN_URL=https://your-provider.com/oauth2/token +REVOKE_TOKEN_URL=https://your-provider.com/oauth2/revoke + +# OAuth Settings +SCOPE=openid profile email +REDIRECT_URI=http://localhost:8501/oauth2callback + +# Authentication Provider +AUTH_PROVIDER=custom + # ProductsAgent API Configuration PRODUCTS_AGENT_URL=http://localhost:8001 ``` diff --git a/products-web/app.py b/products-web/app.py index 68e0b14..7c0fb34 100644 --- a/products-web/app.py +++ b/products-web/app.py @@ -140,22 +140,43 @@ # Environment variables CLIENT_ID = os.environ.get("CLIENT_ID") CLIENT_SECRET = os.environ.get("CLIENT_SECRET") -TENANT_ID = os.environ.get("TENANT_ID") +TENANT_ID = os.environ.get("TENANT_ID") # For Microsoft Entra ID +KEYCLOAK_SERVER_URL = os.environ.get("KEYCLOAK_SERVER_URL") # For Keycloak +KEYCLOAK_REALM = os.environ.get("KEYCLOAK_REALM") # For Keycloak SCOPE = os.environ.get("SCOPE", "openid profile email User.Read") REDIRECT_URI = os.environ.get("REDIRECT_URI", "http://localhost:8501/oauth2callback") -BASE_URL = os.environ.get("BASE_URL", "https://login.microsoftonline.com") +BASE_URL = os.environ.get("BASE_URL") PRODUCTS_AGENT_URL = os.environ.get("PRODUCTS_AGENT_URL", "http://localhost:8000") - -# Construct OAuth URLs dynamically from base URL and tenant ID -if TENANT_ID and BASE_URL: - # Always construct URLs dynamically - no environment variable overrides - AUTHORIZE_URL = f"{BASE_URL}/{TENANT_ID}/oauth2/v2.0/authorize" - TOKEN_URL = f"{BASE_URL}/{TENANT_ID}/oauth2/v2.0/token" - REFRESH_TOKEN_URL = f"{BASE_URL}/{TENANT_ID}/oauth2/v2.0/token" - # Microsoft Entra ID doesn't have a standard token revocation endpoint - REVOKE_TOKEN_URL = None -else: - AUTHORIZE_URL = TOKEN_URL = REFRESH_TOKEN_URL = REVOKE_TOKEN_URL = None +AUTH_PROVIDER = os.environ.get("AUTH_PROVIDER", "entra_id") # 'entra_id' or 'keycloak' + +# Construct OAuth URLs based on the authentication provider +AUTHORIZE_URL = os.environ.get("AUTHORIZE_URL") +TOKEN_URL = os.environ.get("TOKEN_URL") +REFRESH_TOKEN_URL = os.environ.get("REFRESH_TOKEN_URL") +REVOKE_TOKEN_URL = os.environ.get("REVOKE_TOKEN_URL") + +if not all([AUTHORIZE_URL, TOKEN_URL, REFRESH_TOKEN_URL]): + if AUTH_PROVIDER.lower() == "keycloak" and KEYCLOAK_SERVER_URL and KEYCLOAK_REALM: + # Keycloak URL construction + keycloak_base = f"{KEYCLOAK_SERVER_URL}/realms/{KEYCLOAK_REALM}/protocol/openid-connect" + AUTHORIZE_URL = f"{keycloak_base}/auth" + TOKEN_URL = f"{keycloak_base}/token" + REFRESH_TOKEN_URL = f"{keycloak_base}/token" + REVOKE_TOKEN_URL = f"{keycloak_base}/revoke" + if not BASE_URL: + BASE_URL = KEYCLOAK_SERVER_URL + elif TENANT_ID and (BASE_URL or AUTH_PROVIDER.lower() == "entra_id"): + # Microsoft Entra ID URL construction (default behavior) + base_url = BASE_URL or "https://login.microsoftonline.com" + AUTHORIZE_URL = f"{base_url}/{TENANT_ID}/oauth2/v2.0/authorize" + TOKEN_URL = f"{base_url}/{TENANT_ID}/oauth2/v2.0/token" + REFRESH_TOKEN_URL = f"{base_url}/{TENANT_ID}/oauth2/v2.0/token" + # Microsoft Entra ID doesn't have a standard token revocation endpoint + REVOKE_TOKEN_URL = None + if not BASE_URL: + BASE_URL = base_url + else: + AUTHORIZE_URL = TOKEN_URL = REFRESH_TOKEN_URL = REVOKE_TOKEN_URL = None # Initialize session state for chat if "chat_messages" not in st.session_state: @@ -540,7 +561,8 @@ def main(): # Header with logout button col1, col2 = st.columns([4, 1]) with col1: - st.title("đŸĢ† Secure Agentic Demo: HCP Vault x Bedrock x Entra ID ") + provider_display = "Keycloak" if AUTH_PROVIDER.lower() == "keycloak" else "Entra ID" + st.title(f"đŸĢ† Secure Agentic Demo: HCP Vault x Bedrock x {provider_display}") with col2: if "token" in st.session_state: st.markdown('
', unsafe_allow_html=True) @@ -550,35 +572,102 @@ def main(): st.markdown("---") - # Check configuration + # Check configuration based on authentication provider missing_config = [] if not CLIENT_ID: missing_config.append("CLIENT_ID") if not CLIENT_SECRET: missing_config.append("CLIENT_SECRET") - if not TENANT_ID: - missing_config.append("TENANT_ID") + + if AUTH_PROVIDER.lower() == "keycloak": + if not KEYCLOAK_SERVER_URL: + missing_config.append("KEYCLOAK_SERVER_URL") + if not KEYCLOAK_REALM: + missing_config.append("KEYCLOAK_REALM") + else: # Default to Entra ID + if not TENANT_ID: + missing_config.append("TENANT_ID") if missing_config: st.error(f"Missing required environment variables: {', '.join(missing_config)}") - st.info( - "Please create a .env file based on .env.example and fill in your Microsoft Entra ID configuration." - ) + if AUTH_PROVIDER.lower() == "keycloak": + st.info( + "Please create a .env file based on .env.keycloak.example and fill in your Keycloak configuration." + ) + with st.expander("💡 Keycloak Setup Help", expanded=True): + st.markdown(""" + **Required Configuration:** + - `CLIENT_ID`: Your Keycloak client ID + - `CLIENT_SECRET`: Your Keycloak client secret + - `KEYCLOAK_SERVER_URL`: Your Keycloak server URL (e.g., https://keycloak.yourdomain.com) + - `KEYCLOAK_REALM`: Your Keycloak realm name + + **Quick Setup:** + 1. Copy `.env.keycloak.example` to `.env` + 2. Update with your Keycloak server details + 3. Create a client in your Keycloak realm + 4. Set redirect URI to: `http://localhost:8501/oauth2callback` + + 📖 **See [KEYCLOAK.md](./KEYCLOAK.md) for detailed setup instructions** + """) + else: + st.info( + "Please create a .env file based on .env.example and fill in your Microsoft Entra ID configuration." + ) + with st.expander("💡 Microsoft Entra ID Setup Help", expanded=True): + st.markdown(""" + **Required Configuration:** + - `CLIENT_ID`: Your Azure AD application (client) ID + - `CLIENT_SECRET`: Your Azure AD client secret + - `TENANT_ID`: Your Azure AD directory (tenant) ID + + **Quick Setup:** + 1. Copy `.env.example` to `.env` + 2. Register an application in Azure AD + 3. Add redirect URI: `http://localhost:8501/oauth2callback` + 4. Generate a client secret + 5. Copy the IDs into your `.env` file + """) + st.stop() + + # Validate OAuth URL construction + if not AUTHORIZE_URL or not TOKEN_URL: + st.error("❌ OAuth URL construction failed") + if AUTH_PROVIDER.lower() == "keycloak": + st.error("Could not construct Keycloak OAuth URLs. Please verify:") + st.markdown(""" + - `KEYCLOAK_SERVER_URL` is a valid URL (e.g., https://keycloak.yourdomain.com) + - `KEYCLOAK_REALM` is specified + - URLs should not have trailing slashes + """) + else: + st.error("Could not construct Microsoft Entra ID OAuth URLs. Please verify:") + st.markdown(""" + - `TENANT_ID` is specified + - `BASE_URL` is valid (or use default) + """) st.stop() # Authentication flow if "token" not in st.session_state: st.header("đŸšĒ Authentication Required") - st.write( - "Please authenticate with Microsoft Entra ID to access the ProductsAgent chat." - ) + provider_name = "Keycloak" if AUTH_PROVIDER.lower() == "keycloak" else "Microsoft Entra ID" + st.write(f"Please authenticate with {provider_name} to access the ProductsAgent chat.") with st.expander("â„šī¸ Configuration Details", expanded=False): + st.write(f"**Authentication Provider:** {AUTH_PROVIDER}") st.write(f"**Client ID:** {CLIENT_ID}") - st.write(f"**Tenant ID:** {TENANT_ID}") + if AUTH_PROVIDER.lower() == "keycloak": + st.write(f"**Keycloak Server:** {KEYCLOAK_SERVER_URL}") + st.write(f"**Realm:** {KEYCLOAK_REALM}") + else: + st.write(f"**Tenant ID:** {TENANT_ID}") st.write(f"**Base URL:** {BASE_URL}") st.write(f"**Scopes:** {SCOPE}") st.write(f"**Products API:** {PRODUCTS_AGENT_URL}") + if AUTHORIZE_URL: + st.write(f"**Authorize URL:** {AUTHORIZE_URL}") + st.write(f"**Token URL:** {TOKEN_URL}") # Create OAuth2Component instance oauth2 = OAuth2Component( @@ -591,8 +680,9 @@ def main(): ) # Authorization button + button_text = f"Login with {provider_name}" result = oauth2.authorize_button( - "Login with Microsoft", + button_text, REDIRECT_URI, SCOPE, height=600, diff --git a/products-web/screenshots/keycloak-config-expanded.png b/products-web/screenshots/keycloak-config-expanded.png new file mode 100644 index 0000000..7243009 Binary files /dev/null and b/products-web/screenshots/keycloak-config-expanded.png differ diff --git a/products-web/screenshots/keycloak-login-page.png b/products-web/screenshots/keycloak-login-page.png new file mode 100644 index 0000000..5ed2db0 Binary files /dev/null and b/products-web/screenshots/keycloak-login-page.png differ