Add new providers.

This commit is contained in:
2025-09-26 23:08:50 +01:00
parent abea624841
commit 7c8917bf6f
17 changed files with 1587 additions and 53 deletions
+111 -21
View File
@@ -4,14 +4,28 @@ type: middleware
import: github.com/lukaszraczylo/traefikoidc
summary: |
Middleware adding OpenID Connect (OIDC) authentication to Traefik routes.
Universal OpenID Connect (OIDC) authentication middleware for Traefik.
This middleware replaces the need for forward-auth and oauth2-proxy when using Traefik as a reverse proxy.
It provides a complete OIDC authentication solution with features like domain restrictions,
role-based access control, token caching, and more.
role-based access control, token caching, session management, and memory-efficient operation.
The middleware has been tested with Auth0, Logto, Google, and other standard OIDC providers.
It includes special handling for Google's OAuth implementation to ensure compatibility.
🎯 SUPPORTED PROVIDERS (Auto-Detection):
✅ Google - Full OIDC, auto-configured for Workspace
✅ Azure AD - Enterprise OIDC with tenant/group support
✅ Auth0 - Flexible OIDC with custom claims
✅ Okta - Enterprise SSO with MFA support
✅ Keycloak - Self-hosted OIDC with full customization
✅ AWS Cognito - Managed OIDC with regional endpoints
✅ GitLab - Both GitLab.com and self-hosted instances
⚠️ GitHub - OAuth 2.0 only (limited: API access, no user claims)
✅ Generic OIDC - Any RFC-compliant OIDC provider
🔧 KEY FEATURES:
- Automatic provider detection and configuration
- Built-in warnings for provider limitations
- Memory-efficient with bounded resource usage
- Comprehensive test coverage and security monitoring
It supports various authentication scenarios including:
- Basic authentication with customizable callback and logout URLs
@@ -83,13 +97,21 @@ testData:
# --- Provider Specific Configuration Examples ---
#
# Below are example configurations tailored for specific OIDC providers.
# Uncomment and adapt the relevant section for your provider.
# Remember to replace placeholder values (like client IDs, secrets, domains)
# with your actual credentials and settings.
# This middleware supports 9+ OIDC providers with automatic detection:
# ✅ Google - Full OIDC with auto-configuration
# ✅ Azure AD - Enterprise OIDC with tenant support
# ✅ Auth0 - Flexible OIDC with custom claims
# ✅ Okta - Enterprise OIDC with MFA support
# ✅ Keycloak - Self-hosted OIDC with full customization
# ✅ AWS Cognito - Managed OIDC with regional endpoints
# ✅ GitLab - Both GitLab.com and self-hosted
# ⚠️ GitHub - OAuth 2.0 only (not OIDC, limited functionality)
# ✅ Generic OIDC - Any RFC-compliant OIDC provider
#
# Uncomment and adapt the relevant section for your provider.
# Remember to replace placeholder values with your actual credentials.
# For all providers, ensure claims like email, roles, and groups are
# configured to be included in the ID TOKEN. This plugin validates ID tokens.
# configured to be included in the ID TOKEN (this plugin validates ID tokens).
# --- Keycloak Example ---
# testDataKeycloak:
@@ -127,18 +149,81 @@ testData:
# --- Google Workspace / Google Cloud Identity Example ---
# testDataGoogle:
# providerURL: https://accounts.google.com # This is standard for Google
# providerURL: https://accounts.google.com # Standard Google OIDC endpoint
# clientID: your-google-client-id.apps.googleusercontent.com
# clientSecret: your-google-client-secret # Store securely
# callbackURL: /oauth2/callback
# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-google"
# scopes: # Defaults ["openid", "profile", "email"] are handled. Plugin manages Google-specifics.
# # Do NOT add 'offline_access' - plugin handles this.
# allowedUserDomains: # Useful for Google Workspace users
# scopes: # Auto-detects Google and applies proper configuration
# # Do NOT add 'offline_access' - plugin automatically handles Google-specific parameters
# allowedUserDomains: # Useful for Google Workspace domain restriction
# - your-gsuite-domain.com
# # Google includes 'hd' (hosted domain) claim which can be used with allowedUserDomains.
# # Other claims like 'email', 'sub', 'name' are standard.
# # See README.md "Provider Configuration Recommendations" for Google.
# refreshGracePeriodSeconds: 300 # Optional: Refresh 5 min before expiry
# # Google auto-config: Uses access_type=offline, prompt=consent, filters unsupported scopes
# # Available claims: email, sub, name, given_name, family_name, picture, hd (hosted domain)
# --- Okta Example ---
# testDataOkta:
# providerURL: https://your-tenant.okta.com/oauth2/default # Use your Okta domain and auth server
# clientID: your-okta-client-id
# clientSecret: your-okta-client-secret # Store securely
# callbackURL: /oauth2/callback
# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-okta"
# scopes:
# - groups # Include for group-based access control
# allowedRolesAndGroups:
# - admin
# - developer
# - "Everyone" # Default Okta group
# # Okta config: Create OIDC Web App in admin console, configure Groups claim
# # Available claims: email, sub, name, groups, custom attributes
# --- AWS Cognito Example ---
# testDataCognito:
# providerURL: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_YourUserPool # Regional endpoint
# clientID: your-cognito-client-id
# clientSecret: your-cognito-client-secret # Store securely
# callbackURL: /oauth2/callback
# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-cognito"
# scopes:
# - aws.cognito.signin.user.admin # Cognito-specific scope
# allowedRolesAndGroups:
# - admin
# - user
# # Cognito config: Create User Pool, App Client with authorization code grant
# # Available claims: email, sub, cognito:username, cognito:groups, custom attributes
# --- GitLab Example ---
# testDataGitLab:
# providerURL: https://gitlab.com # For GitLab.com, or use your self-hosted URL
# clientID: your-gitlab-client-id
# clientSecret: your-gitlab-client-secret # Store securely
# callbackURL: /oauth2/callback
# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-gitlab"
# scopes:
# - read_user
# - read_api # For GitLab API access
# allowedUserDomains:
# - yourcompany.com # Optional domain restriction
# # GitLab config: Create application in GitLab Admin Area > Applications
# # Available claims: email, sub, name, nickname, preferred_username
# --- GitHub OAuth 2.0 Example (⚠️ Limited Functionality) ---
# testDataGitHub:
# providerURL: https://github.com/login/oauth # GitHub OAuth endpoint (NOT OIDC)
# clientID: your-github-client-id
# clientSecret: your-github-client-secret # Store securely
# callbackURL: /oauth2/callback
# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-github"
# scopes:
# - user:email
# - read:user
# # ⚠️ IMPORTANT: GitHub uses OAuth 2.0, NOT OpenID Connect
# # - No ID tokens available (access tokens only)
# # - No refresh tokens (users must re-authenticate when tokens expire)
# # - No standard OIDC claims
# # - Use only for GitHub API access, not for user authentication with claims
# # GitHub config: Create OAuth App in GitHub Settings > Developer settings
# --- Auth0 Example ---
# testDataAuth0:
@@ -182,11 +267,16 @@ configuration:
The base URL of the OIDC provider. This is the issuer URL that will be used to discover
OIDC endpoints like authorization, token, and JWKS URIs.
Examples:
- https://accounts.google.com
- https://login.microsoftonline.com/tenant-id/v2.0
- https://your-auth0-domain.auth0.com
- https://your-logto-instance.com/oidc
Supported providers (auto-detected from URL):
- https://accounts.google.com (Google)
- https://login.microsoftonline.com/tenant-id/v2.0 (Azure AD)
- https://your-auth0-domain.auth0.com (Auth0)
- https://your-tenant.okta.com/oauth2/default (Okta)
- https://your-keycloak/auth/realms/your-realm (Keycloak)
- https://cognito-idp.region.amazonaws.com/pool-id (AWS Cognito)
- https://gitlab.com (GitLab)
- https://github.com/login/oauth (GitHub - OAuth 2.0 only)
- Any RFC-compliant OIDC provider (Generic)
required: true
clientID:
+372 -27
View File
@@ -5,6 +5,8 @@ This middleware replaces the need for forward-auth and oauth2-proxy when using T
## Overview
The Traefik OIDC middleware provides a complete OIDC authentication solution with features like:
- **Universal provider support**: Works with 9+ OIDC providers including Google, Azure AD, Auth0, Okta, Keycloak, AWS Cognito, GitLab, and more
- **Automatic provider detection**: Automatically detects and configures provider-specific settings
- Token validation and verification
- Session management with automatic cleanup
- Domain restrictions
@@ -14,9 +16,37 @@ The Traefik OIDC middleware provides a complete OIDC authentication solution wit
- Excluded paths (public URLs)
- Memory-efficient operation with bounded resource usage
## Supported OIDC Providers
| Provider | Support Level | Refresh Tokens | Auto-Detection | Key Features |
|----------|---------------|----------------|---------------|--------------|
| **Google** | ✅ Full OIDC | ✅ Yes | ✅ `accounts.google.com` | Auto-config, Workspace support |
| **Azure AD** | ✅ Full OIDC | ✅ Yes | ✅ `login.microsoftonline.com` | Multi-tenant, group claims |
| **Auth0** | ✅ Full OIDC | ✅ Yes | ✅ `*.auth0.com` | Custom claims, flexible rules |
| **Okta** | ✅ Full OIDC | ✅ Yes | ✅ `*.okta.com` | Enterprise SSO, MFA support |
| **Keycloak** | ✅ Full OIDC | ✅ Yes | ✅ `/auth/realms/` path | Self-hosted, full customization |
| **AWS Cognito** | ✅ Full OIDC | ✅ Yes | ✅ `cognito-idp.*.amazonaws.com` | Managed service, regional |
| **GitLab** | ✅ Full OIDC | ✅ Yes | ✅ `gitlab.com` | Self-hosted support |
| **GitHub** | ⚠️ OAuth 2.0 Only | ❌ No | ✅ `github.com` | API access only, no claims |
| **Generic OIDC** | ✅ Full OIDC | ✅ Yes | ✅ Any endpoint | RFC-compliant providers |
### Provider Capabilities Matrix
| Feature | Google | Azure AD | Auth0 | Okta | Keycloak | Cognito | GitLab | GitHub | Generic |
|---------|--------|----------|-------|------|----------|---------|--------|--------|---------|
| **ID Tokens** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| **Refresh Tokens** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| **Custom Claims** | Limited | ✅ | ✅ | ✅ | ✅ | ✅ | Limited | ❌ | Varies |
| **Group/Role Claims** | Limited | ✅ | ✅ | ✅ | ✅ | ✅ | Limited | ❌ | Varies |
| **Domain Restriction** | ✅ (hd claim) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | Varies |
| **Self-Hosted** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ |
| **Enterprise Features** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | Varies |
> **Important**: GitHub uses OAuth 2.0 (not OpenID Connect) and only provides access tokens. Use it for API access only, not for user authentication with claims. All other providers support full OIDC with ID tokens and user claims.
**Important Note on Token Validation:** This middleware performs authentication and claim extraction based on the **ID Token** provided by the OIDC provider. It does not primarily use the Access Token for these purposes (though the Access Token is available for templated headers if needed). Therefore, ensure that all necessary claims (e.g., email, roles, custom attributes) are included in the ID Token by your OIDC provider's configuration.
The middleware has been tested with Auth0, Logto, Google and other standard OIDC providers. It includes special handling for Google's OAuth implementation.
The middleware has been tested with Google, Azure AD, Auth0, Okta, Keycloak, AWS Cognito, GitLab, GitHub (OAuth 2.0), and other standard OIDC providers. It includes automatic provider detection and special handling for provider-specific requirements.
### Performance and Memory Management
@@ -447,9 +477,9 @@ spec:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
```
### Google OIDC Configuration Example
## Provider-Specific Configuration Examples
This example shows a configuration specifically tailored for Google OIDC:
### Google OIDC Configuration
```yaml
apiVersion: traefik.io/v1alpha1
@@ -461,20 +491,197 @@ spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: your-google-client-id.apps.googleusercontent.com # Replace with your Client ID
clientSecret: your-google-client-secret # Replace with your Client Secret
sessionEncryptionKey: your-secure-encryption-key-min-32-chars # Replace with your key
callbackURL: /oauth2/callback # Adjust if needed
logoutURL: /oauth2/logout # Optional: Adjust if needed
clientID: your-google-client-id.apps.googleusercontent.com
clientSecret: your-google-client-secret
sessionEncryptionKey: your-secure-encryption-key-min-32-chars
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
# Note: DO NOT manually add offline_access scope for Google
# The middleware automatically handles Google-specific requirements
refreshGracePeriodSeconds: 300 # Optional: Start refresh 5 min before expiry (default 60)
# Other optional parameters like allowedUserDomains, etc. can be added here
refreshGracePeriodSeconds: 300 # Optional: Start refresh 5 min before expiry
allowedUserDomains:
- your-gsuite-domain.com # Optional: Restrict to workspace users
```
The middleware automatically detects Google as the provider and applies the necessary adjustments to ensure proper authentication and token refresh. See the [Google OAuth Fix](#google-oauth-compatibility-fix) section for details.
### Azure AD Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-azure
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://login.microsoftonline.com/your-tenant-id/v2.0
clientID: your-azure-ad-client-id
clientSecret: your-azure-ad-client-secret
sessionEncryptionKey: your-secure-encryption-key-min-32-chars
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- roles # For group/role claims, configure in Azure AD Token Configuration
allowedUserDomains:
- yourcompany.com
allowedRolesAndGroups:
- "group-object-id-1" # Azure AD group Object IDs
- "AppRoleName" # Application role names
```
### Auth0 Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-auth0
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://your-auth0-domain.auth0.com
clientID: your-auth0-client-id
clientSecret: your-auth0-client-secret
sessionEncryptionKey: your-secure-encryption-key-min-32-chars
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- read:custom_data # Custom scopes as needed
allowedRolesAndGroups:
- "https://your-app.com/roles:admin" # Namespaced claims from Actions
- editor
postLogoutRedirectURI: /logged-out-page # Must be in Auth0 Allowed Logout URLs
```
### Okta Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-okta
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://your-tenant.okta.com/oauth2/default
clientID: your-okta-client-id
clientSecret: your-okta-client-secret
sessionEncryptionKey: your-secure-encryption-key-min-32-chars
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- groups # Include groups in token claims
allowedRolesAndGroups:
- admin
- developer
- "Everyone" # Default Okta group
```
### Keycloak Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-keycloak
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://your-keycloak-domain/auth/realms/your-realm
clientID: your-keycloak-client-id
clientSecret: your-keycloak-client-secret
sessionEncryptionKey: your-secure-encryption-key-min-32-chars
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- roles
- groups
allowedRolesAndGroups:
- admin
- editor
# Ensure Keycloak client mappers add necessary claims to ID Token
```
### AWS Cognito Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-cognito
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_YourUserPool
clientID: your-cognito-client-id
clientSecret: your-cognito-client-secret
sessionEncryptionKey: your-secure-encryption-key-min-32-chars
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- aws.cognito.signin.user.admin # Cognito-specific scope
allowedRolesAndGroups:
- admin
- user
```
### GitLab Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-gitlab
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://gitlab.com
clientID: your-gitlab-client-id
clientSecret: your-gitlab-client-secret
sessionEncryptionKey: your-secure-encryption-key-min-32-chars
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- read_user
- read_api
allowedUserDomains:
- yourcompany.com
```
### GitHub OAuth Configuration ⚠️
**Warning**: GitHub uses OAuth 2.0, not OpenID Connect. Use only for API access, not user authentication.
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oauth-github
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://github.com/login/oauth
clientID: your-github-client-id
clientSecret: your-github-client-secret
sessionEncryptionKey: your-secure-encryption-key-min-32-chars
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- user:email
- read:user
# Note: No ID tokens available, only access tokens for GitHub API
# No refresh tokens - users must re-authenticate when tokens expire
```
The middleware automatically detects each provider and applies the necessary adjustments to ensure proper authentication and token refresh.
### Keeping Secrets Secret in Kubernetes
@@ -776,16 +983,110 @@ This Traefik OIDC plugin performs authentication and extracts user claims (like
This section provides guidance on configuring popular OIDC providers to work optimally with this plugin.
### Google Workspace / Google Cloud Identity
Google's OIDC implementation is well-supported with automatic configuration.
* **Automatic Configuration**: The middleware automatically detects Google and applies required settings:
* Uses `access_type=offline` and `prompt=consent` for refresh tokens
* Filters out unsupported `offline_access` scope
* Handles Google-specific token refresh
* **Setup Requirements**:
* Create OAuth 2.0 credentials in Google Cloud Console
* Configure OAuth consent screen (must be "Published" for production)
* Add authorized redirect URIs
* **ID Token Claims**: Google includes standard claims like `email`, `sub`, `name`, `given_name`, `family_name`, `picture`
* **Hosted Domain**: For Google Workspace, the `hd` claim contains the organization domain
* **Best Practices**: Use `providerURL: https://accounts.google.com`
### Azure AD (Microsoft Entra ID)
Azure AD provides comprehensive enterprise OIDC support.
* **Tenant Configuration**: Use tenant-specific endpoint: `https://login.microsoftonline.com/{tenant-id}/v2.0`
* **Group Claims**: Configure in App Registration → Token Configuration → Add groups claim
* **ID Token Claims**: Includes `email`, `name`, `preferred_username`, `oid` by default
* **Group Handling**: Be aware of group "overage" - too many groups results in a groups claim link instead of embedded groups
* **Optional Claims**: Add custom claims via Token Configuration section
* **Multi-tenant**: Supports both single-tenant and multi-tenant applications
### Auth0
Auth0 provides flexible OIDC with custom claims support.
* **Custom Claims**: Use Auth0 Actions (recommended) or Rules to add claims to ID Token:
```javascript
// Auth0 Action example
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'https://your-app.com/';
if (event.authorization) {
api.idToken.setCustomClaim(namespace + 'roles', event.authorization.roles);
api.idToken.setCustomClaim('email', event.user.email);
}
};
```
* **Logout Configuration**: Ensure `postLogoutRedirectURI` is in "Allowed Logout URLs"
* **Application Type**: Set to "Regular Web Application" for server-side flows
* **Refresh Tokens**: Automatically handled with `offline_access` scope
### Okta
Okta provides enterprise-grade OIDC with extensive customization.
* **Application Setup**: Create OIDC Web Application in Okta Admin Console
* **Authorization Server**: Use default (`/oauth2/default`) or custom authorization server
* **Group Claims**: Configure Groups claim in authorization server to include user groups
* **Scopes**: Default scopes sufficient; add `groups` scope for group information
* **Sign-On Policy**: Configure authentication policies and MFA requirements
* **Custom Claims**: Add custom attributes via user profiles and authorization server claims
### Keycloak
Keycloak is highly configurable, which means you need to ensure your client mappers are set up correctly to include necessary claims in the ID Token.
Keycloak is highly configurable, requiring proper client mapper setup.
* **Ensure Claims in ID Token**:
* **Email**: Navigate to your Keycloak realm -> Clients -> Your Client ID -> Mappers. Ensure there's a mapper for 'email' (e.g., a "User Property" mapper for the `email` property) and that "Add to ID token" is **ON**.
* **Roles**: For client roles or realm roles, create or edit mappers (e.g., "User Client Role" or "User Realm Role"). Ensure "Add to ID token" is **ON**. You might want to customize the "Token Claim Name" (e.g., to `roles` or `groups`).
* **Groups**: Similarly, for group membership, use a "Group Membership" mapper and ensure "Add to ID token" is **ON**. Customize the "Token Claim Name" as needed (e.g., `groups`).
* **Scopes**: Ensure your client requests appropriate scopes that trigger the inclusion of these claims if your mappers are scope-dependent. The default `openid`, `profile`, `email` scopes are a good starting point.
* **Troubleshooting**: If claims are missing, double-check the "Mappers" tab for your client in Keycloak. The "Token Claim Name" you define here is what you'll use in the `allowedRolesAndGroups` or `headers` configuration in this plugin. (See also the [Troubleshooting](#troubleshooting) section for Keycloak).
* **Client Mappers**: Essential for including claims in ID Token:
* **Email**: User Property mapper for `email` with "Add to ID token" enabled
* **Roles**: User Client Role or User Realm Role mappers with "Add to ID token" enabled
* **Groups**: Group Membership mapper with "Add to ID token" enabled
* **Token Claim Names**: Use mapper "Token Claim Name" in `allowedRolesAndGroups` configuration
* **Realm Configuration**: Ensure proper realm settings and client configuration
* **Issuer URL Format**: `https://your-keycloak/auth/realms/your-realm`
* **Troubleshooting**: Verify mappers in Clients → Your Client → Mappers tab
### AWS Cognito
AWS Cognito provides managed OIDC with regional deployment.
* **User Pool Setup**: Create User Pool with proper app client configuration
* **App Client**: Enable "Authorization code grant" and configure callback URLs
* **Regional Endpoints**: Auto-detected from issuer URL format
* **Custom Attributes**: Configure custom attributes and map to claims
* **Groups**: Use Cognito Groups for role-based access control
* **Federation**: Supports federated identity providers (SAML, social providers)
### GitLab
GitLab supports OIDC for both GitLab.com and self-hosted instances.
* **Application Registration**: Create in GitLab Admin Area → Applications
* **Scopes**: Use `openid`, `profile`, `email` for basic claims
* **Self-hosted**: Use your GitLab instance URL as `providerURL`
* **GitLab.com**: Use `https://gitlab.com` as `providerURL`
* **Group Claims**: May require custom configuration for group information
* **API Access**: Include `read_api` scope for GitLab API access via access token
### GitHub (OAuth 2.0 Only) ⚠️
**Important**: GitHub uses OAuth 2.0, not OpenID Connect.
* **OAuth App Setup**: Register OAuth App in GitHub Settings → Developer settings
* **Limitations**:
* No ID tokens (access tokens only)
* No refresh tokens (tokens expire, requiring re-authentication)
* No standard OIDC claims
* **Use Cases**: API access only, not suitable for user authentication with claims
* **Scopes**: Use `user:email`, `read:user` for basic profile access
* **Detection**: Auto-detected from `github.com` in issuer URL
### Azure AD (Microsoft Entra ID)
@@ -872,18 +1173,62 @@ logLevel: debug
- Use double curly braces to escape template expressions: `value: "Bearer {{{{.AccessToken}}}}"`
- This is the only reliable method that works with Traefik's YAML parsing
- See the [Templated Headers](#templated-headers) section for complete examples
7. **Google sessions expire after ~1 hour**: If using Google as the OIDC provider and sessions expire prematurely (around 1 hour instead of longer), ensure:
#### Provider-Specific Issues
7. **Google sessions expire after ~1 hour**: If using Google as the OIDC provider and sessions expire prematurely:
- Do NOT manually add the `offline_access` scope. Google rejects this scope as invalid.
- The middleware automatically applies the required Google parameters (`access_type=offline` and `prompt=consent`).
- Your Google Cloud OAuth consent screen is set to "External" and "Production" mode. "Testing" mode often limits refresh token validity.
- Verify you're using a version of the middleware that includes the Google OAuth compatibility fix.
- For more details, see the [Google OAuth Compatibility Fix](#google-oauth-compatibility-fix) section or the [detailed documentation](docs/google-oauth-fix.md).
- The middleware automatically applies Google parameters (`access_type=offline` and `prompt=consent`).
- Ensure your Google Cloud OAuth consent screen is "Published" for production.
- "Testing" mode limits refresh token validity.
8. **Keycloak: Claims Missing from ID Token (e.g., email, roles)**
8. **Keycloak: Claims Missing from ID Token**:
- Configure client mappers to add email, roles, groups to ID Token
- Check "Add to ID token" is enabled for all required mappers
- Verify "Token Claim Name" matches your configuration
If you are using Keycloak and claims like `email`, `roles`, or `groups` are missing from the ID Token, this plugin may not function as expected (e.g., for domain restrictions or RBAC).
* **Solution**: This plugin validates the **ID Token**. You **must** configure Keycloak client mappers to add all necessary claims (email, roles, groups, etc.) to the ID Token.
* For detailed instructions, please see the [Keycloak](#keycloak) section under [Provider Configuration Recommendations](#provider-configuration-recommendations).
9. **Azure AD: Group overage issues**:
- Users with many groups may receive a groups link instead of embedded groups
- Consider using app roles instead of groups for many-group scenarios
- Configure group claims in App Registration → Token Configuration
10. **Auth0: Custom claims not appearing**:
- Use Auth0 Actions (not Rules) to add custom claims to ID Token
- Ensure namespaced claims follow format: `https://your-app.com/claim`
- Add claims to ID token specifically, not just access token
11. **Okta: Authorization server issues**:
- Verify using correct authorization server endpoint (`/oauth2/default` or custom)
- Ensure Groups claim is configured in authorization server
- Check application assignment and user group membership
12. **AWS Cognito: Regional endpoint errors**:
- Use correct regional endpoint format: `cognito-idp.{region}.amazonaws.com`
- Verify User Pool ID is correct in issuer URL
- Check app client has authorization code grant enabled
13. **GitLab: Self-hosted instance issues**:
- Ensure issuer URL points to your GitLab instance root
- Verify application is created in Admin Area → Applications
- Check redirect URI configuration matches exactly
14. **GitHub: Limited functionality warnings**:
- Remember GitHub is OAuth 2.0 only, not OIDC
- No ID tokens available (access tokens only)
- No refresh tokens (re-authentication required on expiry)
- Use only for GitHub API access, not user authentication
### Provider Warnings and Recommendations
The middleware includes built-in warnings for provider-specific limitations. Check your logs for important notices about:
- **GitHub OAuth 2.0 limitations** (no OIDC support)
- **Auth0 offline_access scope requirements**
- **Keycloak URL pattern requirements**
- **AWS Cognito regional endpoint requirements**
- **Provider-specific setup recommendations**
For detailed provider-specific guidance, see the [Provider-Specific Configuration Examples](#provider-specific-configuration-examples) section.
## Recent Improvements
+72
View File
@@ -0,0 +1,72 @@
package providers
import (
"net/url"
)
// Auth0Provider encapsulates Auth0-specific OIDC logic.
type Auth0Provider struct {
*BaseProvider
}
// NewAuth0Provider creates a new instance of the Auth0Provider.
func NewAuth0Provider() *Auth0Provider {
return &Auth0Provider{
BaseProvider: NewBaseProvider(),
}
}
// GetType returns the provider's type.
func (p *Auth0Provider) GetType() ProviderType {
return ProviderTypeAuth0
}
// GetCapabilities returns the specific capabilities of the Auth0 provider.
func (p *Auth0Provider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{
SupportsRefreshTokens: true,
RequiresOfflineAccessScope: true,
RequiresPromptConsent: false,
PreferredTokenValidation: "id", // Auth0 typically uses ID tokens
}
}
// BuildAuthParams configures Auth0-specific authentication parameters.
func (p *Auth0Provider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
// Auth0 supports various response types and connection parameters
baseParams.Set("response_type", "code")
// Ensure offline_access scope is present for refresh tokens
hasOfflineAccess := false
for _, scope := range scopes {
if scope == "offline_access" {
hasOfflineAccess = true
break
}
}
if !hasOfflineAccess {
scopes = append(scopes, "offline_access")
}
// Ensure openid scope is present
hasOpenID := false
for _, scope := range scopes {
if scope == "openid" {
hasOpenID = true
break
}
}
if !hasOpenID {
scopes = append(scopes, "openid")
}
return &AuthParams{
URLValues: baseParams,
Scopes: scopes,
}, nil
}
// Auth0 requires specific tenant configuration and connection handling.
func (p *Auth0Provider) ValidateConfig() error {
return p.BaseProvider.ValidateConfig()
}
+124
View File
@@ -0,0 +1,124 @@
package providers
import (
"net/url"
"testing"
)
// TestAuth0Provider_NewAuth0Provider tests the constructor
func TestAuth0Provider_NewAuth0Provider(t *testing.T) {
provider := NewAuth0Provider()
if provider == nil {
t.Fatal("Expected provider to be created, got nil")
}
if provider.BaseProvider == nil {
t.Error("BaseProvider should be initialized")
}
}
// TestAuth0Provider_GetType tests provider type
func TestAuth0Provider_GetType(t *testing.T) {
provider := NewAuth0Provider()
if provider.GetType() != ProviderTypeAuth0 {
t.Errorf("Expected ProviderTypeAuth0, got %v", provider.GetType())
}
}
// TestAuth0Provider_GetCapabilities tests Auth0-specific capabilities
func TestAuth0Provider_GetCapabilities(t *testing.T) {
provider := NewAuth0Provider()
capabilities := provider.GetCapabilities()
if !capabilities.SupportsRefreshTokens {
t.Error("Expected SupportsRefreshTokens to be true for Auth0")
}
if !capabilities.RequiresOfflineAccessScope {
t.Error("Expected RequiresOfflineAccessScope to be true for Auth0")
}
if capabilities.RequiresPromptConsent {
t.Error("Expected RequiresPromptConsent to be false for Auth0")
}
if capabilities.PreferredTokenValidation != "id" {
t.Errorf("Expected PreferredTokenValidation 'id', got '%s'", capabilities.PreferredTokenValidation)
}
}
// TestAuth0Provider_BuildAuthParams tests Auth0-specific auth params
func TestAuth0Provider_BuildAuthParams(t *testing.T) {
provider := NewAuth0Provider()
baseParams := url.Values{}
baseParams.Set("client_id", "test_client")
tests := []struct {
name string
scopes []string
expectedScopes []string
}{
{
name: "Add offline_access and openid scopes",
scopes: []string{"profile", "email"},
expectedScopes: []string{"profile", "email", "offline_access", "openid"},
},
{
name: "Keep existing offline_access and openid",
scopes: []string{"openid", "profile", "offline_access", "email"},
expectedScopes: []string{"openid", "profile", "offline_access", "email"},
},
{
name: "Add both scopes when none provided",
scopes: []string{},
expectedScopes: []string{"offline_access", "openid"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
authParams, err := provider.BuildAuthParams(baseParams, tt.scopes)
if err != nil {
t.Errorf("BuildAuthParams failed: %v", err)
return
}
// Check that response_type is set
if authParams.URLValues.Get("response_type") != "code" {
t.Errorf("Expected response_type 'code', got '%s'", authParams.URLValues.Get("response_type"))
}
if len(authParams.Scopes) != len(tt.expectedScopes) {
t.Errorf("Expected %d scopes, got %d. Expected: %v, Got: %v",
len(tt.expectedScopes), len(authParams.Scopes), tt.expectedScopes, authParams.Scopes)
return
}
// Check that all expected scopes are present
for _, expectedScope := range tt.expectedScopes {
found := false
for _, actualScope := range authParams.Scopes {
if actualScope == expectedScope {
found = true
break
}
}
if !found {
t.Errorf("Expected scope '%s' not found in %v", expectedScope, authParams.Scopes)
}
}
})
}
}
// TestAuth0Provider_ValidateConfig tests config validation
func TestAuth0Provider_ValidateConfig(t *testing.T) {
provider := NewAuth0Provider()
err := provider.ValidateConfig()
if err != nil {
t.Errorf("ValidateConfig failed: %v", err)
}
}
+73
View File
@@ -0,0 +1,73 @@
package providers
import (
"net/url"
)
// AWSCognitoProvider encapsulates AWS Cognito-specific OIDC logic.
type AWSCognitoProvider struct {
*BaseProvider
}
// NewAWSCognitoProvider creates a new instance of the AWSCognitoProvider.
func NewAWSCognitoProvider() *AWSCognitoProvider {
return &AWSCognitoProvider{
BaseProvider: NewBaseProvider(),
}
}
// GetType returns the provider's type.
func (p *AWSCognitoProvider) GetType() ProviderType {
return ProviderTypeAWSCognito
}
// GetCapabilities returns the specific capabilities of the AWS Cognito provider.
func (p *AWSCognitoProvider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{
SupportsRefreshTokens: true,
RequiresOfflineAccessScope: false, // Cognito doesn't use offline_access scope
RequiresPromptConsent: false,
PreferredTokenValidation: "id", // Cognito typically uses ID tokens
}
}
// BuildAuthParams configures AWS Cognito-specific authentication parameters.
func (p *AWSCognitoProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
// AWS Cognito supports standard OIDC parameters
baseParams.Set("response_type", "code")
// Remove offline_access scope as Cognito doesn't use it
var filteredScopes []string
for _, scope := range scopes {
if scope != "offline_access" {
filteredScopes = append(filteredScopes, scope)
}
}
// Ensure openid scope is present
hasOpenID := false
for _, scope := range filteredScopes {
if scope == "openid" {
hasOpenID = true
break
}
}
if !hasOpenID {
filteredScopes = append(filteredScopes, "openid")
}
// Default Cognito scopes if none specified
if len(filteredScopes) == 1 && filteredScopes[0] == "openid" {
filteredScopes = append(filteredScopes, "email", "profile")
}
return &AuthParams{
URLValues: baseParams,
Scopes: filteredScopes,
}, nil
}
// AWS Cognito requires user pool and domain configuration.
func (p *AWSCognitoProvider) ValidateConfig() error {
return p.BaseProvider.ValidateConfig()
}
+27 -3
View File
@@ -18,6 +18,12 @@ func NewProviderFactory() *ProviderFactory {
registry.RegisterProvider(NewGenericProvider())
registry.RegisterProvider(NewGoogleProvider())
registry.RegisterProvider(NewAzureProvider())
registry.RegisterProvider(NewGitHubProvider())
registry.RegisterProvider(NewAuth0Provider())
registry.RegisterProvider(NewOktaProvider())
registry.RegisterProvider(NewKeycloakProvider())
registry.RegisterProvider(NewAWSCognitoProvider())
registry.RegisterProvider(NewGitLabProvider())
return &ProviderFactory{
registry: registry,
@@ -65,6 +71,18 @@ func (f *ProviderFactory) CreateProviderByType(providerType ProviderType) (OIDCP
provider = NewGoogleProvider()
case ProviderTypeAzure:
provider = NewAzureProvider()
case ProviderTypeGitHub:
provider = NewGitHubProvider()
case ProviderTypeAuth0:
provider = NewAuth0Provider()
case ProviderTypeOkta:
provider = NewOktaProvider()
case ProviderTypeKeycloak:
provider = NewKeycloakProvider()
case ProviderTypeAWSCognito:
provider = NewAWSCognitoProvider()
case ProviderTypeGitLab:
provider = NewGitLabProvider()
default:
return nil, fmt.Errorf("unsupported provider type: %d", providerType)
}
@@ -79,9 +97,15 @@ func (f *ProviderFactory) CreateProviderByType(providerType ProviderType) (OIDCP
// GetSupportedProviders returns a list of all supported provider types and their detection patterns.
func (f *ProviderFactory) GetSupportedProviders() map[ProviderType][]string {
return map[ProviderType][]string{
ProviderTypeGeneric: {"*"},
ProviderTypeGoogle: {"accounts.google.com"},
ProviderTypeAzure: {"login.microsoftonline.com", "sts.windows.net"},
ProviderTypeGeneric: {"*"},
ProviderTypeGoogle: {"accounts.google.com"},
ProviderTypeAzure: {"login.microsoftonline.com", "sts.windows.net"},
ProviderTypeGitHub: {"github.com"},
ProviderTypeAuth0: {".auth0.com"},
ProviderTypeOkta: {".okta.com", ".oktapreview.com", ".okta-emea.com"},
ProviderTypeKeycloak: {"keycloak"},
ProviderTypeAWSCognito: {"cognito-idp", ".amazonaws.com"},
ProviderTypeGitLab: {"gitlab.com"},
}
}
+42
View File
@@ -53,6 +53,48 @@ func TestProviderFactory_CreateProvider(t *testing.T) {
expectedType: ProviderTypeAzure,
wantErr: false,
},
{
name: "GitHub provider",
issuerURL: "https://github.com/login/oauth",
expectedType: ProviderTypeGitHub,
wantErr: false,
},
{
name: "Auth0 provider",
issuerURL: "https://tenant.auth0.com",
expectedType: ProviderTypeAuth0,
wantErr: false,
},
{
name: "Okta provider",
issuerURL: "https://tenant.okta.com",
expectedType: ProviderTypeOkta,
wantErr: false,
},
{
name: "Okta preview provider",
issuerURL: "https://tenant.oktapreview.com",
expectedType: ProviderTypeOkta,
wantErr: false,
},
{
name: "Keycloak provider",
issuerURL: "https://auth.example.com/auth/realms/master",
expectedType: ProviderTypeKeycloak,
wantErr: false,
},
{
name: "AWS Cognito provider",
issuerURL: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example",
expectedType: ProviderTypeAWSCognito,
wantErr: false,
},
{
name: "GitLab provider",
issuerURL: "https://gitlab.com/oauth",
expectedType: ProviderTypeGitLab,
wantErr: false,
},
{
name: "Generic provider",
issuerURL: "https://auth.example.com",
+61
View File
@@ -0,0 +1,61 @@
package providers
import (
"net/url"
)
// GitHubProvider encapsulates GitHub-specific OIDC logic.
type GitHubProvider struct {
*BaseProvider
}
// NewGitHubProvider creates a new instance of the GitHubProvider.
func NewGitHubProvider() *GitHubProvider {
return &GitHubProvider{
BaseProvider: NewBaseProvider(),
}
}
// GetType returns the provider's type.
func (p *GitHubProvider) GetType() ProviderType {
return ProviderTypeGitHub
}
// GetCapabilities returns the specific capabilities of the GitHub provider.
// WARNING: GitHub does NOT support OpenID Connect - it's OAuth 2.0 only.
// This provider should only be used for OAuth flows, not OIDC authentication.
func (p *GitHubProvider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{
SupportsRefreshTokens: false, // GitHub OAuth apps don't support refresh tokens
RequiresOfflineAccessScope: false, // GitHub doesn't use offline_access
RequiresPromptConsent: false,
PreferredTokenValidation: "access", // GitHub only provides access tokens, no ID tokens
}
}
// BuildAuthParams configures GitHub-specific authentication parameters.
func (p *GitHubProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
// GitHub doesn't use offline_access scope, so remove it if present
var filteredScopes []string
for _, scope := range scopes {
if scope != "offline_access" {
filteredScopes = append(filteredScopes, scope)
}
}
// If no scopes specified, use default GitHub scopes for OAuth
// Note: GitHub doesn't support 'openid' scope as it's not an OIDC provider
if len(filteredScopes) == 0 {
filteredScopes = []string{"user:email", "read:user"}
}
return &AuthParams{
URLValues: baseParams,
Scopes: filteredScopes,
}, nil
}
// GitHub requires specific configuration for proper operation.
func (p *GitHubProvider) ValidateConfig() error {
return p.BaseProvider.ValidateConfig()
}
+110
View File
@@ -0,0 +1,110 @@
package providers
import (
"net/url"
"testing"
)
// TestGitHubProvider_NewGitHubProvider tests the constructor
func TestGitHubProvider_NewGitHubProvider(t *testing.T) {
provider := NewGitHubProvider()
if provider == nil {
t.Fatal("Expected provider to be created, got nil")
}
if provider.BaseProvider == nil {
t.Error("BaseProvider should be initialized")
}
}
// TestGitHubProvider_GetType tests provider type
func TestGitHubProvider_GetType(t *testing.T) {
provider := NewGitHubProvider()
if provider.GetType() != ProviderTypeGitHub {
t.Errorf("Expected ProviderTypeGitHub, got %v", provider.GetType())
}
}
// TestGitHubProvider_GetCapabilities tests GitHub-specific capabilities
func TestGitHubProvider_GetCapabilities(t *testing.T) {
provider := NewGitHubProvider()
capabilities := provider.GetCapabilities()
if capabilities.SupportsRefreshTokens {
t.Error("Expected SupportsRefreshTokens to be false for GitHub")
}
if capabilities.RequiresOfflineAccessScope {
t.Error("Expected RequiresOfflineAccessScope to be false for GitHub")
}
if capabilities.RequiresPromptConsent {
t.Error("Expected RequiresPromptConsent to be false for GitHub")
}
if capabilities.PreferredTokenValidation != "access" {
t.Errorf("Expected PreferredTokenValidation 'access', got '%s'", capabilities.PreferredTokenValidation)
}
}
// TestGitHubProvider_BuildAuthParams tests GitHub-specific auth params
func TestGitHubProvider_BuildAuthParams(t *testing.T) {
provider := NewGitHubProvider()
baseParams := url.Values{}
baseParams.Set("client_id", "test_client")
tests := []struct {
name string
scopes []string
expectedScopes []string
}{
{
name: "Remove offline_access scope",
scopes: []string{"user:email", "offline_access", "read:user"},
expectedScopes: []string{"user:email", "read:user"},
},
{
name: "Default scopes when none provided",
scopes: []string{},
expectedScopes: []string{"user:email", "read:user"},
},
{
name: "Keep other scopes",
scopes: []string{"user", "repo"},
expectedScopes: []string{"user", "repo"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
authParams, err := provider.BuildAuthParams(baseParams, tt.scopes)
if err != nil {
t.Errorf("BuildAuthParams failed: %v", err)
return
}
if len(authParams.Scopes) != len(tt.expectedScopes) {
t.Errorf("Expected %d scopes, got %d", len(tt.expectedScopes), len(authParams.Scopes))
return
}
for i, scope := range tt.expectedScopes {
if authParams.Scopes[i] != scope {
t.Errorf("Expected scope '%s', got '%s'", scope, authParams.Scopes[i])
}
}
})
}
}
// TestGitHubProvider_ValidateConfig tests config validation
func TestGitHubProvider_ValidateConfig(t *testing.T) {
provider := NewGitHubProvider()
err := provider.ValidateConfig()
if err != nil {
t.Errorf("ValidateConfig failed: %v", err)
}
}
+73
View File
@@ -0,0 +1,73 @@
package providers
import (
"net/url"
)
// GitLabProvider encapsulates GitLab-specific OIDC logic.
type GitLabProvider struct {
*BaseProvider
}
// NewGitLabProvider creates a new instance of the GitLabProvider.
func NewGitLabProvider() *GitLabProvider {
return &GitLabProvider{
BaseProvider: NewBaseProvider(),
}
}
// GetType returns the provider's type.
func (p *GitLabProvider) GetType() ProviderType {
return ProviderTypeGitLab
}
// GetCapabilities returns the specific capabilities of the GitLab provider.
func (p *GitLabProvider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{
SupportsRefreshTokens: true,
RequiresOfflineAccessScope: false, // GitLab doesn't use offline_access scope
RequiresPromptConsent: false,
PreferredTokenValidation: "id", // GitLab typically uses ID tokens
}
}
// BuildAuthParams configures GitLab-specific authentication parameters.
func (p *GitLabProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
// GitLab supports standard OAuth 2.0 parameters
baseParams.Set("response_type", "code")
// Remove offline_access scope as GitLab doesn't use it
var filteredScopes []string
for _, scope := range scopes {
if scope != "offline_access" {
filteredScopes = append(filteredScopes, scope)
}
}
// Ensure openid scope is present for OIDC
hasOpenID := false
for _, scope := range filteredScopes {
if scope == "openid" {
hasOpenID = true
break
}
}
if !hasOpenID {
filteredScopes = append(filteredScopes, "openid")
}
// Default GitLab scopes if none specified
if len(filteredScopes) == 1 && filteredScopes[0] == "openid" {
filteredScopes = append(filteredScopes, "profile", "email")
}
return &AuthParams{
URLValues: baseParams,
Scopes: filteredScopes,
}, nil
}
// GitLab requires application configuration and proper redirect URIs.
func (p *GitLabProvider) ValidateConfig() error {
return p.BaseProvider.ValidateConfig()
}
+2 -2
View File
@@ -24,8 +24,8 @@ func (p *GoogleProvider) GetType() ProviderType {
// GetCapabilities returns the specific capabilities of the Google provider.
func (p *GoogleProvider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{
SupportsRefreshTokens: true,
RequiresOfflineAccessScope: false,
SupportsRefreshTokens: true, // Google DOES support refresh tokens
RequiresOfflineAccessScope: false, // Google uses access_type=offline instead
RequiresPromptConsent: true,
PreferredTokenValidation: "id",
}
+6
View File
@@ -25,6 +25,12 @@ const (
ProviderTypeGeneric ProviderType = iota
ProviderTypeGoogle
ProviderTypeAzure
ProviderTypeGitHub
ProviderTypeAuth0
ProviderTypeOkta
ProviderTypeKeycloak
ProviderTypeAWSCognito
ProviderTypeGitLab
)
// ProviderCapabilities defines the specific features and behaviors of an OIDC provider.
+72
View File
@@ -0,0 +1,72 @@
package providers
import (
"net/url"
)
// KeycloakProvider encapsulates Keycloak-specific OIDC logic.
type KeycloakProvider struct {
*BaseProvider
}
// NewKeycloakProvider creates a new instance of the KeycloakProvider.
func NewKeycloakProvider() *KeycloakProvider {
return &KeycloakProvider{
BaseProvider: NewBaseProvider(),
}
}
// GetType returns the provider's type.
func (p *KeycloakProvider) GetType() ProviderType {
return ProviderTypeKeycloak
}
// GetCapabilities returns the specific capabilities of the Keycloak provider.
func (p *KeycloakProvider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{
SupportsRefreshTokens: true,
RequiresOfflineAccessScope: true,
RequiresPromptConsent: false,
PreferredTokenValidation: "id", // Keycloak typically uses ID tokens
}
}
// BuildAuthParams configures Keycloak-specific authentication parameters.
func (p *KeycloakProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
// Keycloak supports standard OIDC parameters
baseParams.Set("response_type", "code")
// Ensure offline_access scope is present for refresh tokens
hasOfflineAccess := false
for _, scope := range scopes {
if scope == "offline_access" {
hasOfflineAccess = true
break
}
}
if !hasOfflineAccess {
scopes = append(scopes, "offline_access")
}
// Ensure openid scope is present
hasOpenID := false
for _, scope := range scopes {
if scope == "openid" {
hasOpenID = true
break
}
}
if !hasOpenID {
scopes = append(scopes, "openid")
}
return &AuthParams{
URLValues: baseParams,
Scopes: scopes,
}, nil
}
// Keycloak requires realm and server configuration.
func (p *KeycloakProvider) ValidateConfig() error {
return p.BaseProvider.ValidateConfig()
}
+72
View File
@@ -0,0 +1,72 @@
package providers
import (
"net/url"
)
// OktaProvider encapsulates Okta-specific OIDC logic.
type OktaProvider struct {
*BaseProvider
}
// NewOktaProvider creates a new instance of the OktaProvider.
func NewOktaProvider() *OktaProvider {
return &OktaProvider{
BaseProvider: NewBaseProvider(),
}
}
// GetType returns the provider's type.
func (p *OktaProvider) GetType() ProviderType {
return ProviderTypeOkta
}
// GetCapabilities returns the specific capabilities of the Okta provider.
func (p *OktaProvider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{
SupportsRefreshTokens: true,
RequiresOfflineAccessScope: true,
RequiresPromptConsent: false,
PreferredTokenValidation: "id", // Okta primarily uses ID tokens
}
}
// BuildAuthParams configures Okta-specific authentication parameters.
func (p *OktaProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
// Okta supports various response types
baseParams.Set("response_type", "code")
// Ensure offline_access scope is present for refresh tokens
hasOfflineAccess := false
for _, scope := range scopes {
if scope == "offline_access" {
hasOfflineAccess = true
break
}
}
if !hasOfflineAccess {
scopes = append(scopes, "offline_access")
}
// Ensure openid scope is present
hasOpenID := false
for _, scope := range scopes {
if scope == "openid" {
hasOpenID = true
break
}
}
if !hasOpenID {
scopes = append(scopes, "openid")
}
return &AuthParams{
URLValues: baseParams,
Scopes: scopes,
}, nil
}
// Okta requires specific domain configuration and application setup.
func (p *OktaProvider) ValidateConfig() error {
return p.BaseProvider.ValidateConfig()
}
+24
View File
@@ -134,6 +134,30 @@ func (r *ProviderRegistry) detectProviderUnsafe(issuerURL string) OIDCProvider {
if strings.Contains(host, "login.microsoftonline.com") || strings.Contains(host, "sts.windows.net") {
return p
}
case ProviderTypeGitHub:
if strings.Contains(host, "github.com") {
return p
}
case ProviderTypeAuth0:
if strings.Contains(host, ".auth0.com") {
return p
}
case ProviderTypeOkta:
if strings.Contains(host, ".okta.com") || strings.Contains(host, ".oktapreview.com") || strings.Contains(host, ".okta-emea.com") {
return p
}
case ProviderTypeKeycloak:
if strings.Contains(host, "keycloak") || strings.Contains(normalizedURL.Path, "/auth/realms/") {
return p
}
case ProviderTypeAWSCognito:
if strings.Contains(host, "cognito-idp") && strings.Contains(host, ".amazonaws.com") {
return p
}
case ProviderTypeGitLab:
if strings.Contains(host, "gitlab.com") {
return p
}
}
}
+151
View File
@@ -0,0 +1,151 @@
package providers
import (
"fmt"
"strings"
)
// ProviderWarning represents a warning about provider limitations or requirements.
type ProviderWarning struct {
ProviderType ProviderType
Level string // "info", "warning", "error"
Message string
}
// GetProviderWarnings returns warnings about provider-specific limitations.
func GetProviderWarnings(providerType ProviderType) []ProviderWarning {
var warnings []ProviderWarning
switch providerType {
case ProviderTypeGitHub:
warnings = append(warnings, ProviderWarning{
ProviderType: ProviderTypeGitHub,
Level: "warning",
Message: "GitHub uses OAuth 2.0, not OpenID Connect. ID tokens are not available. Use access tokens for API calls only.",
})
warnings = append(warnings, ProviderWarning{
ProviderType: ProviderTypeGitHub,
Level: "info",
Message: "GitHub OAuth apps do not support refresh tokens. Users will need to re-authenticate when tokens expire.",
})
case ProviderTypeAuth0:
warnings = append(warnings, ProviderWarning{
ProviderType: ProviderTypeAuth0,
Level: "info",
Message: "Auth0 requires 'offline_access' scope for refresh tokens. This will be automatically added.",
})
case ProviderTypeOkta:
warnings = append(warnings, ProviderWarning{
ProviderType: ProviderTypeOkta,
Level: "info",
Message: "Okta requires proper application configuration in your Okta admin console for OIDC to work.",
})
case ProviderTypeKeycloak:
warnings = append(warnings, ProviderWarning{
ProviderType: ProviderTypeKeycloak,
Level: "info",
Message: "Keycloak detection is based on URL path '/auth/realms/'. Ensure your issuer URL follows this pattern.",
})
case ProviderTypeAWSCognito:
warnings = append(warnings, ProviderWarning{
ProviderType: ProviderTypeAWSCognito,
Level: "info",
Message: "AWS Cognito uses regional endpoints. Ensure your issuer URL includes the correct region (e.g., cognito-idp.us-east-1.amazonaws.com).",
})
case ProviderTypeGitLab:
warnings = append(warnings, ProviderWarning{
ProviderType: ProviderTypeGitLab,
Level: "info",
Message: "GitLab supports OIDC but requires application registration in GitLab admin settings.",
})
}
return warnings
}
// ValidateProviderCompatibility checks if a provider is suitable for OIDC authentication.
func ValidateProviderCompatibility(providerType ProviderType, requiresOIDC bool) error {
switch providerType {
case ProviderTypeGitHub:
if requiresOIDC {
return fmt.Errorf("GitHub does not support OpenID Connect. It only supports OAuth 2.0. Consider using a different provider for OIDC authentication")
}
return nil
default:
return nil
}
}
// GetProviderRecommendations returns setup recommendations for each provider.
func GetProviderRecommendations(providerType ProviderType) []string {
switch providerType {
case ProviderTypeGitHub:
return []string{
"Register an OAuth App in GitHub Settings > Developer settings > OAuth Apps",
"Use scopes: 'user:email', 'read:user' for basic profile access",
"GitHub tokens expire, plan for re-authentication flow",
}
case ProviderTypeAuth0:
return []string{
"Create an Application in Auth0 Dashboard",
"Set Application Type to 'Regular Web Application'",
"Configure Allowed Callback URLs with your redirect URI",
"Enable 'offline_access' scope for refresh tokens",
}
case ProviderTypeOkta:
return []string{
"Create an App Integration in Okta Admin Console",
"Choose 'OIDC - OpenID Connect' as sign-in method",
"Select 'Web Application' as application type",
"Configure redirect URIs and assign users/groups",
}
case ProviderTypeKeycloak:
return []string{
"Create a Client in your Keycloak realm",
"Set Client Protocol to 'openid-connect'",
"Configure Valid Redirect URIs",
"Ensure issuer URL format: https://your-keycloak/auth/realms/your-realm",
}
case ProviderTypeAWSCognito:
return []string{
"Create a User Pool in AWS Cognito",
"Create an App Client with 'Authorization code grant' enabled",
"Configure App Client settings and callback URLs",
"Use issuer URL format: https://cognito-idp.{region}.amazonaws.com/{userPoolId}",
}
case ProviderTypeGitLab:
return []string{
"Create an Application in GitLab (Admin Area > Applications)",
"Select 'openid', 'profile', 'email' scopes",
"Configure Redirect URI",
"Use issuer URL: https://gitlab.com (for GitLab.com)",
}
default:
return []string{}
}
}
// FormatProviderWarnings formats warnings for display.
func FormatProviderWarnings(warnings []ProviderWarning) string {
if len(warnings) == 0 {
return ""
}
var result strings.Builder
for _, warning := range warnings {
result.WriteString(fmt.Sprintf("[%s] %s\n", strings.ToUpper(warning.Level), warning.Message))
}
return result.String()
}
+195
View File
@@ -0,0 +1,195 @@
package providers
import (
"strings"
"testing"
)
// TestGetProviderWarnings tests that warnings are provided for providers with limitations
func TestGetProviderWarnings(t *testing.T) {
tests := []struct {
name string
providerType ProviderType
expectCount int
checkContent string
}{
{
name: "GitHub has OAuth 2.0 warning",
providerType: ProviderTypeGitHub,
expectCount: 2,
checkContent: "OAuth 2.0",
},
{
name: "Auth0 has offline_access info",
providerType: ProviderTypeAuth0,
expectCount: 1,
checkContent: "offline_access",
},
{
name: "Okta has configuration info",
providerType: ProviderTypeOkta,
expectCount: 1,
checkContent: "admin console",
},
{
name: "AWS Cognito has regional endpoint info",
providerType: ProviderTypeAWSCognito,
expectCount: 1,
checkContent: "regional endpoints",
},
{
name: "Generic provider has no warnings",
providerType: ProviderTypeGeneric,
expectCount: 0,
checkContent: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
warnings := GetProviderWarnings(tt.providerType)
if len(warnings) != tt.expectCount {
t.Errorf("Expected %d warnings, got %d", tt.expectCount, len(warnings))
}
if tt.checkContent != "" {
found := false
for _, warning := range warnings {
if strings.Contains(strings.ToLower(warning.Message), strings.ToLower(tt.checkContent)) {
found = true
break
}
}
if !found {
t.Errorf("Expected warning content '%s' not found", tt.checkContent)
}
}
})
}
}
// TestValidateProviderCompatibility tests OIDC compatibility validation
func TestValidateProviderCompatibility(t *testing.T) {
tests := []struct {
name string
providerType ProviderType
requiresOIDC bool
expectError bool
}{
{
name: "GitHub with OIDC requirement should error",
providerType: ProviderTypeGitHub,
requiresOIDC: true,
expectError: true,
},
{
name: "GitHub without OIDC requirement should pass",
providerType: ProviderTypeGitHub,
requiresOIDC: false,
expectError: false,
},
{
name: "Auth0 with OIDC requirement should pass",
providerType: ProviderTypeAuth0,
requiresOIDC: true,
expectError: false,
},
{
name: "Google with OIDC requirement should pass",
providerType: ProviderTypeGoogle,
requiresOIDC: true,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateProviderCompatibility(tt.providerType, tt.requiresOIDC)
if tt.expectError && err == nil {
t.Error("Expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
}
}
// TestGetProviderRecommendations tests that recommendations are provided
func TestGetProviderRecommendations(t *testing.T) {
tests := []struct {
name string
providerType ProviderType
expectMin int
}{
{
name: "GitHub recommendations",
providerType: ProviderTypeGitHub,
expectMin: 3,
},
{
name: "Auth0 recommendations",
providerType: ProviderTypeAuth0,
expectMin: 3,
},
{
name: "AWS Cognito recommendations",
providerType: ProviderTypeAWSCognito,
expectMin: 3,
},
{
name: "Generic provider no recommendations",
providerType: ProviderTypeGeneric,
expectMin: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
recommendations := GetProviderRecommendations(tt.providerType)
if len(recommendations) < tt.expectMin {
t.Errorf("Expected at least %d recommendations, got %d", tt.expectMin, len(recommendations))
}
})
}
}
// TestFormatProviderWarnings tests warning formatting
func TestFormatProviderWarnings(t *testing.T) {
warnings := []ProviderWarning{
{
ProviderType: ProviderTypeGitHub,
Level: "warning",
Message: "Test warning message",
},
{
ProviderType: ProviderTypeGitHub,
Level: "info",
Message: "Test info message",
},
}
formatted := FormatProviderWarnings(warnings)
if !strings.Contains(formatted, "[WARNING]") {
t.Error("Expected formatted output to contain [WARNING]")
}
if !strings.Contains(formatted, "[INFO]") {
t.Error("Expected formatted output to contain [INFO]")
}
if !strings.Contains(formatted, "Test warning message") {
t.Error("Expected formatted output to contain warning message")
}
// Test empty warnings
emptyFormatted := FormatProviderWarnings([]ProviderWarning{})
if emptyFormatted != "" {
t.Error("Expected empty string for no warnings")
}
}