diff --git a/docs/deployment/authentication/okta-auth.mdx b/docs/deployment/authentication/okta-auth.mdx
new file mode 100644
index 0000000000..544e570aa8
--- /dev/null
+++ b/docs/deployment/authentication/okta-auth.mdx
@@ -0,0 +1,101 @@
+---
+title: "Okta Authentication"
+---
+
+This document provides information about the Okta integration in Keep.
+
+## Overview
+
+Keep supports Okta as an authentication provider, enabling:
+- Single Sign-On (SSO) via Okta
+- JWT token validation with JWKS
+- User and group management through Okta
+- Role-based access control
+- Token refresh capabilities
+
+## Environment Variables
+
+### Backend Environment Variables
+
+| Variable | Description | Example |
+|----------|-------------|---------|
+| `AUTH_TYPE` | Set to `"OKTA"` to enable Okta authentication | `OKTA` |
+| `OKTA_DOMAIN` | Your Okta domain | `company.okta.com` |
+| `OKTA_API_TOKEN` | Admin API token for Okta management | `00aBcD3f4GhIJkl5m6NoPQr` |
+| `OKTA_ISSUER` | The issuer URL for your Okta application | `https://company.okta.com/oauth2/default` |
+| `OKTA_CLIENT_ID` | Client ID of your Okta application | `0oa1b2c3d4e5f6g7h8i9j` |
+| `OKTA_CLIENT_SECRET` | Client Secret of your Okta application | `abcd1234efgh5678ijkl9012` |
+| `OKTA_JWKS_URL` | (Optional) JWKS URL for your Okta application | `https://company.okta.com/oauth2/default/v1/keys`
+| `OKTA_AUDIENCE` | (Optional) Audience for token validation | `api://keep` |
+| `OKTA_ADMIN_ROLE` | (Optional) Role mapped to the Keep admin role | `KeepAdmin` |
+| `OKTA_NOC_ROLE` | (Optional) Role mapped to the Keep noc role | `KeepNoc` |
+| `OKTA_WEBHOOK_ROLE` | (Optional) Role mapped to the Keep webhook role | `KeepWebhook` |
+| `OKTA_AUTO_CREATE_USER` | (Optional) Try to auto‑create users in Keep when they log in | `True` |
+
+### Frontend Environment Variables
+
+| Variable | Description | Example |
+|----------|-------------|---------|
+| `AUTH_TYPE` | Set to `"OKTA"` to enable Okta authentication | `OKTA` |
+| `OKTA_DOMAIN` | Your Okta domain | `company.okta.com` |
+| `OKTA_CLIENT_ID` | Client ID of your Okta application | `0oa1b2c3d4e5f6g7h8i9j` |
+| `OKTA_CLIENT_SECRET` | Client Secret of your Okta application | `abcd1234efgh5678ijkl9012` |
+| `OKTA_ISSUER` | The issuer URL for your Okta application | `https://company.okta.com/oauth2/default` |
+
+## Okta Configuration
+
+### Creating an Okta Application
+
+1. Sign in to your Okta Admin Console
+2. Navigate to **Applications** > **Applications**
+3. Click **Create App Integration**
+4. Select **OIDC - OpenID Connect** as the Sign-in method
+5. Choose **Web Application** as the Application type
+6. Click **Next**
+
+### Application Settings
+
+1. **Name**: Enter a name for your application (e.g., "Keep")
+2. **Grant type**: Select **Authorization Code**
+3. **Sign-in redirect URIs**: Enter your app's callback URL, e.g., `https://your-keep-domain.com/api/auth/callback/okta`
+4. **Sign-out redirect URIs**: Enter your app's sign-out URL, e.g., `https://your-keep-domain.com/signin`
+5. **Assignments**:
+ - **Skip group assignment for now** or assign to appropriate groups
+6. Click **Save**
+
+### Create API Token
+
+1. Navigate to **Security** > **API**
+2. Select the **Tokens** tab
+3. Click **Create Token**
+4. Name your token (e.g., "Keep Integration")
+5. Copy the generated token value – this is your `OKTA_API_TOKEN`
+
+### Configure OIDC Groups Claims
+
+1. Open your application in the Okta console
+2. Go to the **Sign On** tab
+3. Under **OpenID Connect ID Token**, click **Edit**
+4. Set the **Groups claim filter**:
+ - Claim name: `groups`
+ - Filter: e.g., `Matches regex .*` (returns all groups assigned to the user)
+
+
+
+
+
+## Roles
+
+Keep roles are implemented through Okta groups. By default, the following group mappings are configured:
+
+| Okta Group | Keep Role |
+|----------|-------------|
+| `keep_admin` | `admin` |
+| `keep_noc` | `noc` |
+| `keep_webhook` | `webhook` |
+
+You can override these defaults by setting the backend environment variables:
+
+- `OKTA_ADMIN_ROLE` – role mapped to the Keep `admin` role
+- `OKTA_NOC_ROLE` – role mapped to the Keep `noc` role
+- `OKTA_WEBHOOK_ROLE` – role mapped to the Keep `webhook` role
diff --git a/docs/deployment/authentication/overview.mdx b/docs/deployment/authentication/overview.mdx
index b32703f599..84753b8369 100644
--- a/docs/deployment/authentication/overview.mdx
+++ b/docs/deployment/authentication/overview.mdx
@@ -26,13 +26,13 @@ Choosing the right authentication strategy depends on your specific use case, se
| Identity Provider | RBAC | SAML/OIDC/SSO | LDAP | Resource-based permission | User Management | Group Management | On Prem | License |
|:---:|:----:|:---------:|:----:|:-------------------------:|:----------------:|:-----------------:|:-------:|:-------:|
| **No Auth** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | **OSS** |
-| **DB** | ✅
(Predefiend roles) | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | **OSS** |
-| **Auth0** | ✅
(Predefiend roles) | ✅ | 🚧 | 🚧 | ✅ | 🚧 | ❌ | **EE** |
+| **DB** | ✅
(Predefined roles) | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | **OSS** |
+| **Auth0** | ✅
(Predefined roles) | ✅ | 🚧 | 🚧 | ✅ | 🚧 | ❌ | **EE** |
| **Keycloak** | ✅
(Custom roles) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | **EE** |
-| **Oauth2Proxy** | ✅
(Predefiend roles) | ✅ | ❌ | ❌ | N/A | N/A | ✅ | **OSS** |
-| **Azure AD** | ✅
(Predefiend roles) | ✅ | ❌ | ❌ | By Azure AD | By Azure AD | ✅ | **EE** |
-| **Okta** | ✅
(Predefiend roles) | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | **OSS** |
-| **OneLogin** | ✅
(Predefiend roles) | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | **OSS** |
+| **Oauth2Proxy** | ✅
(Predefined roles) | ✅ | ❌ | ❌ | N/A | N/A | ✅ | **OSS** |
+| **Azure AD** | ✅
(Predefined roles) | ✅ | ❌ | ❌ | By Azure AD | By Azure AD | ✅ | **EE** |
+| **Okta** | ✅
(Predefined roles) | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | **OSS** |
+| **OneLogin** | ✅
(Predefined roles) | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | **OSS** |
### How To Configure
Some authentication providers require additional environment variables. These will be covered in detail on the specific authentication provider pages.
@@ -48,7 +48,7 @@ The authentication scheme on Keep is controlled with environment variables both
| **Keycloak** | `AUTH_TYPE=KEYCLOAK` | `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_CLIENT_ID`, `KEYCLOAK_CLIENT_SECRET` |
| **Oauth2Proxy** | `AUTH_TYPE=OAUTH2PROXY` | `OAUTH2_PROXY_USER_HEADER`, `OAUTH2_PROXY_ROLE_HEADER`, `OAUTH2_PROXY_AUTO_CREATE_USER` |
| **AzureAD** | `AUTH_TYPE=AZUREAD` | See [AzureAD Configuration](/deployment/authentication/azuread-auth) |
-| **Okta** | `AUTH_TYPE=OKTA` | `OKTA_DOMAIN`, `OKTA_CLIENT_ID`, `OKTA_CLIENT_SECRET` |
+| **Okta** | `AUTH_TYPE=OKTA` | See [Okta Configuration](/deployment/authentication/okta-auth) |
| **OneLogin** | `AUTH_TYPE=ONELOGIN` | See [OneLogin Configuration](/deployment/authentication/onelogin-auth) |
For more details on each authentication strategy, including setup instructions and implications, refer to the respective sections.
diff --git a/docs/images/okta.png b/docs/images/okta.png
new file mode 100644
index 0000000000..98539d5b1b
Binary files /dev/null and b/docs/images/okta.png differ
diff --git a/keep/identitymanager/identity_managers/okta/okta_authverifier.py b/keep/identitymanager/identity_managers/okta/okta_authverifier.py
index 615859e8dc..fa50281ba4 100644
--- a/keep/identitymanager/identity_managers/okta/okta_authverifier.py
+++ b/keep/identitymanager/identity_managers/okta/okta_authverifier.py
@@ -1,52 +1,69 @@
import logging
-import os
-
import jwt
from fastapi import Depends, HTTPException
-
+from keep.api.core.config import config
+from keep.api.core.db import (
+ user_exists,
+ create_user,
+ update_user_last_sign_in,
+ update_user_role,
+)
+from keep.api.core.dependencies import SINGLE_TENANT_UUID
from keep.identitymanager.authenticatedentity import AuthenticatedEntity
from keep.identitymanager.authverifierbase import AuthVerifierBase, oauth2_scheme
+from keep.identitymanager.rbac import get_role_by_role_name
logger = logging.getLogger(__name__)
-# Define constant locally instead of importing it
-DEFAULT_ROLE_NAME = "user" # Default role name for user access
class OktaAuthVerifier(AuthVerifierBase):
"""Handles authentication and authorization for Okta"""
def __init__(self, scopes: list[str] = []) -> None:
super().__init__(scopes)
- self.okta_issuer = os.environ.get("OKTA_ISSUER")
- self.okta_audience = os.environ.get("OKTA_AUDIENCE")
- self.okta_client_id = os.environ.get("OKTA_CLIENT_ID")
- self.jwks_url = os.environ.get("OKTA_JWKS_URL")
-
+ self.logger.info(f"Initializing Okta AuthVerifier with scopes: {scopes}")
+ self.okta_issuer = config("OKTA_ISSUER")
+ self.okta_audience = config("OKTA_AUDIENCE")
+ self.okta_client_id = config("OKTA_CLIENT_ID")
+ self.jwks_url = config("OKTA_JWKS_URL")
+ self.auto_create_user = config("OKTA_AUTO_CREATE_USER", default=True)
+
+ self.role_mappings = {
+ config("OKTA_ADMIN_ROLE", default="keep_admin"): "admin",
+ config("OKTA_NOC_ROLE", default="keep_noc"): "noc",
+ config("OKTA_WEBHOOK_ROLE", default="keep_webhook"): "webhook",
+ }
+
# If no explicit JWKS URL is provided, we need an issuer to construct it
if not self.jwks_url and not self.okta_issuer:
- raise Exception("Missing both OKTA_JWKS_URL and OKTA_ISSUER environment variables")
-
+ raise Exception(
+ "Missing both OKTA_JWKS_URL and OKTA_ISSUER environment variables"
+ )
+
# Remove trailing slash if present on issuer
if self.okta_issuer and self.okta_issuer.endswith("/"):
self.okta_issuer = self.okta_issuer[:-1]
-
+
# Initialize JWKS client - prefer direct JWKS URL if available
if not self.jwks_url:
- self.jwks_url = f"{self.okta_issuer}/.well-known/jwks.json"
-
+ self.jwks_url = f"{self.okta_issuer}/v1/keys"
+
# At this point, self.jwks_url is guaranteed to be a string
assert self.jwks_url is not None
self.jwks_client = jwt.PyJWKClient(self.jwks_url)
- logger.info(f"Initialized JWKS client with URL: {self.jwks_url}")
+ self.logger.info(f"Initialized Okta JWKS client with URL: {self.jwks_url}")
+
+ self.logger.info("Okta Auth Verifier initialized")
- def _verify_bearer_token(self, token: str = Depends(oauth2_scheme)) -> AuthenticatedEntity:
+ def _verify_bearer_token(
+ self, token: str = Depends(oauth2_scheme)
+ ) -> AuthenticatedEntity:
if not token:
raise HTTPException(status_code=401, detail="No token provided")
-
try:
# Get the signing key directly from the JWT
signing_key = self.jwks_client.get_signing_key_from_jwt(token).key
-
+
# Decode and verify the token
payload = jwt.decode(
token,
@@ -54,47 +71,123 @@ def _verify_bearer_token(self, token: str = Depends(oauth2_scheme)) -> Authentic
algorithms=["RS256"],
audience=self.okta_audience or self.okta_client_id,
issuer=self.okta_issuer,
- options={"verify_exp": True}
+ options={"verify_exp": True},
)
-
+
# Extract user info from token with simplified role handling
- tenant_id = payload.get("keep_tenant_id", "keep") # Default to 'keep' if not specified
- email = payload.get("email") or payload.get("sub") or payload.get("preferred_username")
-
- # Look for role in standard locations with a default of "user"
- groups = payload.get("groups", [])
- role_name = (
- payload.get("keep_role") or
- payload.get("role") or
- (groups[0] if groups else None) or
- DEFAULT_ROLE_NAME # Use constant for consistency
+ tenant_id = payload.get(
+ "keep_tenant_id", SINGLE_TENANT_UUID
+ ) # Default to SINGLE_TENANT_UUID if not specified
+ user_name = (
+ payload.get("email")
+ or payload.get("sub")
+ or payload.get("preferred_username")
+ )
+
+ okta_groups = payload.get("groups", [])
+
+ okta_groups = [g.strip() for g in okta_groups]
+
+ self.logger.debug(f"Okta Groups: {okta_groups}")
+
+ # Define the priority order of roles
+ role_priority = ["admin", "noc", "webhook"]
+ mapped_role = None
+
+ self.logger.debug(f"Okta to Keep Role Mapping: {self.role_mappings}")
+
+ for role in role_priority:
+ self.logger.debug(f"Checking for role {role}")
+ for okta_group in okta_groups:
+ self.logger.debug(f"Checking for okta group {okta_group}")
+ mapped_role_name = self.role_mappings.get(okta_group, "")
+ self.logger.debug(
+ f"Checking for mapped role name {mapped_role_name}"
+ )
+ if role == mapped_role_name:
+ try:
+ self.logger.debug(f"Getting role {mapped_role_name}")
+ mapped_role = get_role_by_role_name(mapped_role_name)
+ self.logger.debug(f"Role {mapped_role_name} found")
+ break
+ except HTTPException:
+ self.logger.debug(f"Role {mapped_role_name} not found")
+ continue
+ if mapped_role:
+ self.logger.debug(f"Role {mapped_role.get_name()} found")
+ break
+ # if no valid role was found, throw a 403 exception
+ if not mapped_role:
+ self.logger.warning(
+ f"No valid role-group mapping found among {okta_groups}"
+ )
+ raise HTTPException(
+ status_code=403,
+ detail=f"No valid role found among {okta_groups}",
+ )
+
+ if not user_name:
+ raise HTTPException(status_code=401, detail="No user name in token")
+
+ # auto provision user
+ if self.auto_create_user and not user_exists(
+ tenant_id=tenant_id, username=user_name
+ ):
+ self.logger.info(f"Auto provisioning user: {user_name}")
+ create_user(
+ tenant_id=tenant_id,
+ username=user_name,
+ role=mapped_role.get_name(),
+ password="",
+ )
+ self.logger.info(f"User {user_name} created")
+ elif user_exists(tenant_id=tenant_id, username=user_name):
+ # update last login
+ self.logger.debug(f"Updating last login for user: {user_name}")
+ try:
+ update_user_last_sign_in(tenant_id=tenant_id, username=user_name)
+ self.logger.debug(f"Last login updated for user: {user_name}")
+ except Exception:
+ self.logger.warning(
+ f"Failed to update last login for user: {user_name}"
+ )
+ pass
+ # update role
+ self.logger.debug(f"Updating role for user: {user_name}")
+ try:
+ update_user_role(
+ tenant_id=tenant_id,
+ username=user_name,
+ role=mapped_role.get_name(),
+ )
+ self.logger.debug(f"Role updated for user: {user_name}")
+ except Exception:
+ self.logger.warning(f"Failed to update role for user: {user_name}")
+ pass
+
+ self.logger.info(
+ f"User {user_name} authenticated with role {mapped_role.get_name()}"
)
-
- org_id = payload.get("org_id")
- org_realm = payload.get("org_realm")
-
- if not email:
- raise HTTPException(status_code=401, detail="No email in token")
-
- logger.info(f"Successfully verified token for user with email: {email}")
return AuthenticatedEntity(
tenant_id=tenant_id,
- email=email,
- role=role_name,
- org_id=org_id,
- org_realm=org_realm,
- token=token
+ email=user_name,
+ role=mapped_role.get_name(),
+ token=token,
)
-
+
except jwt.exceptions.InvalidKeyError as e:
- logger.error(f"Invalid key error during token validation: {str(e)}")
- raise HTTPException(status_code=401, detail="Invalid signing key - token validation failed")
+ self.logger.error(f"Invalid key error during token validation: {str(e)}")
+ raise HTTPException(
+ status_code=401, detail="Invalid signing key - token validation failed"
+ )
except jwt.ExpiredSignatureError:
- logger.warning("Token has expired")
+ self.logger.warning("Token has expired")
raise HTTPException(status_code=401, detail="Token has expired")
except jwt.InvalidTokenError as e:
- logger.warning(f"Invalid token: {str(e)}")
+ self.logger.warning(f"Invalid token: {str(e)}")
raise HTTPException(status_code=401, detail=f"Invalid token: {str(e)}")
except Exception as e:
- logger.exception("Failed to validate token")
- raise HTTPException(status_code=401, detail=f"Token validation failed: {str(e)}")
\ No newline at end of file
+ self.logger.exception("Failed to validate token")
+ raise HTTPException(
+ status_code=401, detail=f"Token validation failed: {str(e)}"
+ )