Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 102 additions & 1 deletion docs/authentication.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Authentication

Albert Python SDK supports three authentication methods:
Albert Python SDK supports four authentication methods:

* **Single Sign-On (SSO)** via browser-based OAuth2
* **OIDC Token Exchange** for server-to-server integrations using any OIDC-compliant identity provider
* **Client Credentials** using a client ID and secret
* **Static Token** using a pre-generated token (via the `ALBERT_TOKEN` environment variable)

Expand Down Expand Up @@ -46,6 +47,106 @@ client = Albert.from_sso(

---

## 🔄 OIDC Token Exchange

This method is for applications that already authenticate users through an **OIDC-compliant
identity provider** and want to access the Albert API on their behalf — without any browser
interaction. Your application obtains an OIDC ID token and the SDK exchanges it for an Albert
access token automatically.

!!! warning "Tenant configuration required"
This authentication method requires your identity provider's `aud` (audience/client ID)
to be registered with Albert for your tenant. Without this, all requests will return `401 Unauthorized`.
[Contact Albert support](https://support.albertinvent.com/en/contact-us) to enable this for your organisation.

### Supported Identity Providers

Any OIDC-compliant identity provider that includes the `preferred_username` claim in the ID
token is supported. This includes:

| Provider | Notes |
|---|---|
| Microsoft Entra ID (Azure AD) | Supported via v2.0 endpoints |
| Okta | OIDC app integrations only (not SAML) |
| Auth0 | Supported |
| AWS Cognito | User pools with OIDC |
| Ping Identity | PingOne and PingFederate OIDC |
| Keycloak | Supported |
| OneLogin, ForgeRock, IBM Security Verify | Supported |
| Salesforce Identity | OIDC configuration required |
| Self-hosted OIDC servers | Authentik, Dex, Hydra, etc. |

!!! warning "Google Identity / Workspace"
`preferred_username` is not emitted by default in Google's OIDC tokens. You must configure
a custom claim mapping in your Google OAuth app to include it before this flow will work.

!!! info "SAML providers are not supported"
Identity providers configured to use SAML only (rather than OIDC) are not compatible with
this authentication method.

### Prerequisites

- An application registration with your identity provider, with the Albert API audience
registered by Albert support
- A mechanism in your application to obtain an OIDC ID token from your provider

### Usage

Provide a callable that returns a fresh OIDC ID token on demand. The SDK calls it on the
first request and again whenever the Albert access token needs to be renewed.

```python
from albert import Albert
import requests

def get_token() -> str:
resp = requests.post(
"https://mycompany.okta.com/oauth2/default/v1/token",
data={
"grant_type": "client_credentials",
"client_id": "your-okta-client-id",
"client_secret": "your-okta-client-secret",
"scope": "openid",
},
)
return resp.json()["id_token"]

client = Albert.from_sso_exchange(
base_url="https://mycompany.albertinvent.com",
subdomain="mycompany",
oidc_token_provider=get_token,
)
```

Or wire it up manually using `AlbertSSOTokenExchange` directly:

```python
from albert import Albert, AlbertSSOTokenExchange

def get_token() -> str:
# Acquire an ID token from your identity provider
...

auth = AlbertSSOTokenExchange(
base_url="https://mycompany.albertinvent.com",
subdomain="mycompany",
oidc_token_provider=get_token,
)
client = Albert(auth_manager=auth)
```

!!! warning "Use a callable, not a static token string"
Passing `lambda: "my-static-token"` works only while that OIDC token remains valid
(typically 60–90 minutes). Once it expires and the Albert access token needs renewal,
the exchange will fail. Always pass a callable that fetches a fresh token from your IdP.

!!! warning "Token validity is your responsibility"
The SDK passes the token returned by `oidc_token_provider` directly to Albert. If your
callable returns an expired or invalid token, the exchange will fail with `401 Unauthorized`.
Ensure your token acquisition logic handles refresh appropriately.

---

## 🔑 Client Credentials (Programmatic Access)

This method implements the OAuth2 Client Credentials flow and is suitable for non-interactive usage, like backend services or automation scripts. It manages token acquisition and refresh automatically via the `AlbertClientCredentials` class.
Expand Down
3 changes: 3 additions & 0 deletions docs/sso_exchange.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# OIDC Token Exchange

::: albert.AlbertSSOTokenExchange
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ nav:
- Authentication:
- Albert Client Credentials: credentials.md
- Albert SSO Client: sso.md
- OIDC Token Exchange: sso_exchange.md
- Collections:
- Activities: collections/activities.md
- Attachments: collections/attachments.md
Expand Down
9 changes: 8 additions & 1 deletion src/albert/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from albert.client import Albert, AsyncAlbert
from albert.core.auth.credentials import AlbertClientCredentials
from albert.core.auth.sso import AlbertSSOClient
from albert.core.auth.sso_exchange import AlbertSSOTokenExchange

__all__ = ["Albert", "AsyncAlbert", "AlbertClientCredentials", "AlbertSSOClient"]
__all__ = [
"Albert",
"AsyncAlbert",
"AlbertClientCredentials",
"AlbertSSOClient",
"AlbertSSOTokenExchange",
]

__version__ = "1.25.0"
114 changes: 110 additions & 4 deletions src/albert/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
from collections.abc import Callable

from pydantic import SecretStr

Expand Down Expand Up @@ -56,6 +57,7 @@
from albert.core.async_session import AsyncAlbertSession
from albert.core.auth.credentials import AlbertClientCredentials
from albert.core.auth.sso import AlbertSSOClient
from albert.core.auth.sso_exchange import AlbertSSOTokenExchange
from albert.core.session import AlbertSession
from albert.utils._auth import default_albert_base_url

Expand All @@ -76,7 +78,7 @@ class Albert:
token : str, optional
A static token for authentication. If provided, it overrides any `auth_manager`.
Defaults to the "ALBERT_TOKEN" environment variable.
auth_manager : AlbertClientCredentials | AlbertSSOClient, optional
auth_manager : AlbertClientCredentials | AlbertSSOClient | AlbertSSOTokenExchange, optional
An authentication manager for OAuth2-based authentication flows.
Ignored if `token` is provided.
retries : int, optional
Expand All @@ -103,14 +105,18 @@ class Albert:
- `from_token` — Create a client using a static token.
- `from_sso` — Create a client using interactive browser-based SSO login.
- `from_client_credentials` — Create a client using OAuth2 client credentials.
- `from_sso_exchange` — Create a client using server-to-server OIDC token exchange.
"""

def __init__(
self,
*,
base_url: str | None = None,
token: str | None = None,
auth_manager: AlbertClientCredentials | AlbertSSOClient | None = None,
auth_manager: AlbertClientCredentials
| AlbertSSOClient
| AlbertSSOTokenExchange
| None = None,
retries: int | None = None,
session: AlbertSession | None = None,
):
Expand Down Expand Up @@ -169,6 +175,65 @@ def from_client_credentials(
)
return cls(auth_manager=creds, retries=retries)

@classmethod
def from_sso_exchange(
cls,
*,
base_url: str | None = None,
subdomain: str,
oidc_token_provider: Callable[[], str],
retries: int | None = None,
) -> Albert:
"""Create an Albert client using server-to-server OIDC token exchange.

Exchanges an OpenID Connect ID token for an Albert access token without
any browser interaction. Works with any OIDC-compliant identity provider
that emits the ``preferred_username`` claim.

Requires tenant-level OIDC configuration — the OpenID Connect ``aud`` claim
must be registered with Albert for the target tenant.

Parameters
----------
base_url : str | None, optional
The base URL of the Albert API. Falls back to the ``ALBERT_BASE_URL``
environment variable or "https://app.albertinvent.com".
subdomain : str
The tenant subdomain (e.g. ``"mycompany"``).
oidc_token_provider : Callable[[], str]
A zero-argument callable that returns a fresh OIDC ID token on demand.
Called on the first request and on every token renewal.
A lambda returning a static string works for short-lived sessions.
retries : int | None, optional
Maximum number of retries for failed HTTP requests.

Returns
-------
Albert
A configured client that exchanges and refreshes tokens automatically.

Examples
--------
```python
def get_oidc_token() -> str:
# Acquire an ID token from your identity provider
...

client = Albert.from_sso_exchange(
base_url="https://mycompany.albertinvent.com",
subdomain="mycompany",
oidc_token_provider=get_oidc_token,
)
```
"""
resolved_base_url = base_url or default_albert_base_url()
auth = AlbertSSOTokenExchange(
base_url=resolved_base_url,
subdomain=subdomain,
oidc_token_provider=oidc_token_provider,
)
return cls(auth_manager=auth, retries=retries)

@property
def projects(self) -> ProjectCollection:
return ProjectCollection(session=self.session)
Expand Down Expand Up @@ -374,7 +439,7 @@ class AsyncAlbert:
token : str, optional
A static JWT token. Ignored when ``auth_manager`` is provided.
Defaults to the ``ALBERT_TOKEN`` environment variable.
auth_manager : AlbertClientCredentials | AlbertSSOClient, optional
auth_manager : AlbertClientCredentials | AlbertSSOClient | AlbertSSOTokenExchange, optional
An authentication manager for OAuth2-based token refresh.
Ignored if ``token`` is provided.
session : AsyncAlbertSession, optional
Expand Down Expand Up @@ -419,7 +484,10 @@ def __init__(
*,
base_url: str | None = None,
token: str | None = None,
auth_manager: AlbertClientCredentials | AlbertSSOClient | None = None,
auth_manager: AlbertClientCredentials
| AlbertSSOClient
| AlbertSSOTokenExchange
| None = None,
session: AsyncAlbertSession | None = None,
):
if session is not None:
Expand Down Expand Up @@ -495,6 +563,44 @@ def from_client_credentials(
)
return cls(auth_manager=creds)

@classmethod
def from_sso_exchange(
cls,
*,
base_url: str | None = None,
subdomain: str,
oidc_token_provider: Callable[[], str],
) -> AsyncAlbert:
"""
Create an AsyncAlbert client using server-to-server OIDC token exchange.

Works with any OIDC-compliant identity provider that emits the
``preferred_username`` claim. Requires tenant-level OIDC configuration —
contact Albert support to enable.

Parameters
----------
base_url : str | None, optional
The base URL of the Albert API. Falls back to the ``ALBERT_BASE_URL``
environment variable or "https://app.albertinvent.com".
subdomain : str
The tenant subdomain (e.g. ``"mycompany"``).
oidc_token_provider : Callable[[], str]
A zero-argument callable that returns a fresh OIDC ID token on demand.

Returns
-------
AsyncAlbert
A configured async client that exchanges and refreshes tokens automatically.
"""
resolved_base_url = base_url or default_albert_base_url()
auth = AlbertSSOTokenExchange(
base_url=resolved_base_url,
subdomain=subdomain,
oidc_token_provider=oidc_token_provider,
)
return cls(auth_manager=auth)

@property
def chat_sessions(self) -> ChatSessionCollection:
return ChatSessionCollection(session=self.session)
Expand Down
Loading