Authentication¶
Supported IdPs¶
Duar proxies authentication from three identity providers. Provider registration is conditional -- if the environment variables are not set, the provider is not available.
| Provider | Protocol | PKCE | Scopes |
|---|---|---|---|
| OIDC | S256 | openid email profile |
|
| GitHub | OAuth2 | None | user:email |
| Microsoft Entra ID | OIDC | S256 | openid email profile |
Google -- Standard OIDC with automatic discovery. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.
GitHub -- OAuth2 only (not OIDC, no PKCE). User info fetched via GitHub API. If the primary email is not in the profile response, it is fetched from GET /user/emails. Set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET.
Entra ID -- OIDC with tenant-specific discovery. Set ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET, and ENTRA_TENANT_ID. The provider identifier is entra_id.
Entra's claims differ from the OIDC baseline in two ways that affect sign-in:
-
No
email_verifiedclaim. Entra never emits it; its analogue is the optionalxms_edov("email domain owner verified") claim. Duar pins the issuer to the single tenant inENTRA_TENANT_ID, so a token bearing thattidis treated as carrying a tenant-verified address — unlessxms_edovis explicitlyfalse, which is a hard reject. Tokens from any other tenant, and from any other IdP, get no such treatment and must carryemail_verified: true; the exemption is bound to the provider the signature was verified against, not to the presence of atidclaim.Enable the
xms_edovoptional claimWithout it, Duar is trusting your tenant directory rather than an explicit per-address assertion. That is the normal trust model for a single-tenant deployment, but it means anyone who can influence the directory attribute behind the
emailclaim can influence which address Duar sees — and the email drives organization resolution andADMIN_EMAILSauto-promotion. Addingxms_edovto the app registration's token configuration turns that into a verified assertion Duar enforces.emailis often absent. Managed work accounts only receive it if the app registration addsemailas an optional ID-token claim;*.onmicrosoft.comdev-tenant accounts typically have no mail attribute at all. Duar then falls back topreferred_username(the UPN) when it is address-shaped. Identity is always keyed onsub, never on either address.
Since the email domain drives organization resolution, register the org domain that matches the addresses your tenant actually issues (e.g. tptdevelorg.onmicrosoft.com for a dev tenant), or sign-in is refused as "not permitted for this email domain".
For AuthZ mode, the browser goes to Entra directly with response_type=id_token, so the redirect URI must be registered under the Web platform with ID tokens (used for implicit and hybrid flows) enabled — a redirect URI registered under the Single-page application platform rejects implicit. Alternatively, acquire the ID token however you like (e.g. MSAL with auth code + PKCE) and hand it to POST /authz/resolve yourself.
See How Duar Works for the full login flows in both AuthZ and Proxy modes.
Token Types¶
| Token | Audience | TTL | Purpose |
|---|---|---|---|
| Access | duar:access |
15 min | Identity + authorization in Proxy mode. Carries user info, workspace context, and group memberships. |
| Refresh | duar:refresh |
7 days | Silent renewal of access tokens. Supports rotation with reuse detection. |
| Admin | duar:admin |
60 min | Admin panel sessions. Carries admin: true flag. |
| Authz | duar:authz |
5 min | Authorization-only in AuthZ mode. Carries workspace role and RBAC actions. No identity -- identity comes from the IdP token. |
All tokens are RS256-signed JWTs. The algorithm is hardcoded at both encode and decode time to prevent algorithm substitution attacks. Audience validation is mandatory on every decode call.
JWT Claims¶
Access Token Claims¶
| Claim | Type | Example | Description |
|---|---|---|---|
iss |
string | https://auth.example.com |
Issuer. Duar's BASE_URL. |
sub |
string (UUID) | "d4f5a..." |
User ID. |
jti |
string (UUID) | "8b3c1..." |
Unique token ID. Enables per-token revocation via Redis denylist. |
aud |
string | "duar:access" |
Audience. Always duar:access. |
email |
string | "alice@co.com" |
User's email address. |
name |
string | "Alice Chen" |
User's display name. |
wid |
string (UUID) | "a1b2c..." |
Workspace ID. |
wslug |
string | "acme" |
Workspace slug. |
wrole |
string | "editor" |
Workspace role: owner, admin, editor, or viewer. |
groups |
string[] | ["uuid1", "uuid2"] |
Group IDs the user belongs to in this workspace. |
iat |
number | 1709827200 |
Issued-at timestamp (UTC). |
exp |
number | 1709828100 |
Expiration timestamp (UTC). iat + 15 minutes. |
type |
string | "access" |
Token type discriminator. |
Authz Token Claims¶
| Claim | Type | Example | Description |
|---|---|---|---|
iss |
string | https://auth.example.com |
Issuer. Duar's BASE_URL. |
sub |
string (UUID) | "d4f5a..." |
User ID. |
jti |
string (UUID) | "8b3c1..." |
Unique token ID. Enables revocation via denylist. |
aud |
string | "duar:authz" |
Audience. Always duar:authz. |
idp_sub |
string | "104523..." |
IdP subject identifier. Binds this token to a specific IdP identity. The backend validates that the IdP token's sub matches this value. |
svc |
string | "docu-store" |
Service name. Binds the token to a specific service, preventing cross-service replay. |
wid |
string (UUID) | "a1b2c..." |
Workspace ID. |
wslug |
string | "acme" |
Workspace slug. |
wrole |
string | "editor" |
Workspace role. |
actions |
string[] | ["docs:read", "docs:write"] |
RBAC actions granted to this user for this service in this workspace. |
iat |
number | 1709827200 |
Issued-at timestamp (UTC). |
exp |
number | 1709827500 |
Expiration timestamp (UTC). iat + 5 minutes. |
type |
string | "authz" |
Token type discriminator. |
Refresh Token Claims¶
| Claim | Type | Example | Description |
|---|---|---|---|
iss |
string | https://auth.example.com |
Issuer. |
sub |
string (UUID) | "d4f5a..." |
User ID. |
jti |
string (UUID) | "8b3c1..." |
Unique token ID. |
aud |
string | "duar:refresh" |
Audience. Always duar:refresh. |
fid |
string (UUID) | "f7e8d..." |
Family ID. Groups refresh tokens into rotation families for reuse detection. |
iat |
number | 1709827200 |
Issued-at timestamp (UTC). |
exp |
number | 1710432000 |
Expiration timestamp (UTC). iat + 7 days. |
type |
string | "refresh" |
Token type discriminator. |
Admin Token Claims¶
| Claim | Type | Example | Description |
|---|---|---|---|
iss |
string | https://auth.example.com |
Issuer. |
sub |
string (UUID) | "d4f5a..." |
User ID. |
jti |
string (UUID) | "8b3c1..." |
Unique token ID. |
aud |
string | "duar:admin" |
Audience. Always duar:admin. |
email |
string | "alice@co.com" |
Admin user's email. |
name |
string | "Alice Chen" |
Admin user's display name. |
admin |
boolean | true |
Always true. |
iat |
number | 1709827200 |
Issued-at timestamp (UTC). |
exp |
number | 1709830800 |
Expiration timestamp (UTC). iat + 60 minutes. |
type |
string | "admin_access" |
Token type discriminator. |
Token Lifecycle¶
Refresh Rotation with Reuse Detection¶
Refresh tokens use a family-based rotation scheme:
- On login, a refresh token is issued with a unique
fid(family ID). - When the client refreshes, the old refresh token is consumed and a new one is issued with the same
fid. - If a consumed refresh token is presented again (reuse), the entire family is invalidated -- all tokens sharing that
fidare denied.
This detects token theft: if an attacker steals a refresh token and uses it, either the attacker or the legitimate user will trigger reuse detection, invalidating the family.
Revocation¶
Individual tokens are revoked by adding their jti to a Redis denylist. The denylist entry TTL matches the token's remaining lifetime, so entries self-clean. Every token validation checks the denylist before accepting the token.
Logout revokes both the access token and the refresh token's entire family.