Skip to content

OIDC Integration

jitsudo uses OpenID Connect (OIDC) for all authentication. The CLI authenticates users via the Device Authorization Flow (RFC 8628) and the server validates JWTs issued by your IdP.

  1. CLI → IdP: jitsudo login starts the device flow, directing the user to authenticate in any browser.
  2. IdP → CLI: After browser authentication, the IdP issues an ID token (JWT).
  3. CLI → Server: Every API request carries the ID token as a Bearer token in the Authorization header.
  4. Server → IdP: jitsudod validates the token by fetching the IdP’s JWKS from {oidc_issuer}/.well-known/openid-configuration and verifying the JWT signature, issuer (iss), audience (aud), and expiry (exp).
auth:
# Must match the `iss` claim in tokens issued by your IdP.
oidc_issuer: "https://your-idp.example.com"
# OIDC client ID registered with your IdP for the jitsudo server.
client_id: "jitsudo-server"

You need two OIDC clients registered with your IdP:

  • jitsudo-server — the server’s resource server / audience
  • jitsudo-cli — the CLI’s public client (uses device flow, hardcoded in the CLI)

The CLI requests these scopes:

ScopePurpose
openidRequired for OIDC
emailUsed as the user’s identity in requests and audit log
profileDisplay name
groupsGroup membership (used in eligibility/approval policies)
offline_accessRefresh token (for future silent refresh support)

Make sure your IdP includes the groups claim in the ID token.


  1. In Okta Admin Console: Applications → Create App Integration → OIDC → Web Application.
  2. Set Grant type: Device Authorization.
  3. Note the Client ID (jitsudo-server) — this is your client_id.
  4. Set Issuer to your Okta org URL: https://your-org.okta.com.
  1. Applications → Create App Integration → OIDC → Native Application.
  2. Set Grant type: Device Authorization.
  3. Set Client ID to jitsudo-cli (or note the generated one and configure it in the CLI source if needed).

In the Sign-On tab of the server app, edit the token settings:

  1. Add a claim: Name groups, Include in ID Token, Value type Groups, Filter Matches regex: .*.
auth:
oidc_issuer: "https://your-org.okta.com"
client_id: "jitsudo-server"
Terminal window
jitsudo login --provider https://your-org.okta.com

Terminal window
az ad app create \
--display-name "jitsudo-server" \
--identifier-uris "api://jitsudo-server"
# Note the appId — this is your client_id
az ad app show --display-name "jitsudo-server" --query appId -o tsv

Enable device flow on the registration:

Terminal window
# In Azure Portal: App registrations → your app → Authentication
# Add platform: Mobile and desktop applications
# Enable "Allow public client flows"
  1. App registrations → your app → Token configuration → Add groups claim.
  2. Select Security groups and include in ID Token.
Terminal window
az ad app create --display-name "jitsudo-cli"
# Enable device flow and "Allow public client flows"
# The appId becomes the CLI's client ID (must match "jitsudo-cli" constant in source)
auth:
oidc_issuer: "https://login.microsoftonline.com/<TENANT_ID>/v2.0"
client_id: "<SERVER_APP_CLIENT_ID>"
Terminal window
jitsudo login --provider "https://login.microsoftonline.com/<TENANT_ID>/v2.0"

  1. In Keycloak Admin Console, create a realm (e.g. jitsudo).
  2. Create a client: Clients → Create → Client ID: jitsudo-server.
    • Protocol: openid-connect
    • Access Type: confidential (or public for development)
    • Enable Device Authorization Grant.
  3. Create a client: Client ID: jitsudo-cli.
    • Protocol: openid-connect
    • Access Type: public
    • Enable Device Authorization Grant.

For each client, add a mapper:

  • Mapper type: Group Membership
  • Token Claim Name: groups
  • Add to ID token: ON
auth:
oidc_issuer: "https://keycloak.example.com/realms/jitsudo"
client_id: "jitsudo-server"
Terminal window
jitsudo login --provider https://keycloak.example.com/realms/jitsudo

In Google Cloud Console:

  1. APIs & Services → Credentials → Create Credentials → OAuth client ID.
  2. Application type: Desktop app (supports device flow via workaround).
  3. Create one for the server (jitsudo-server) and one for the CLI (jitsudo-cli).

Dex federates to Google Workspace and provides standard RFC 8628 device flow:

dex-config.yaml
connectors:
- type: google
id: google
name: Google
config:
clientID: <GOOGLE_CLIENT_ID>
clientSecret: <GOOGLE_CLIENT_SECRET>
redirectURI: https://dex.example.com/callback
hostedDomains:
- your-org.com
auth:
oidc_issuer: "https://dex.example.com"
client_id: "jitsudo-server"

jitsudo trusts the groups claim in the OIDC ID token without independent verification. This means:

  • Your IdP is the authoritative source of group membership
  • If an attacker can modify group claims at your IdP (e.g., through a compromised admin account or IdP misconfiguration), they could satisfy approval policies they should not
  • Audit your IdP group membership regularly
  • Restrict who can modify group assignments in your IdP

Validation on every API call. jitsudo validates the JWT signature, issuer (iss), audience (aud), and expiry (exp) on every API request by fetching and caching your IdP’s JWKS from {oidc_issuer}/.well-known/openid-configuration. There is no session state held server-side — every request is independently verified against the live JWKS. A token that was valid at login is re-validated on each subsequent call.

What happens when a token expires mid-session. When a JWT expires, the server returns a 401 Unauthorized response. The CLI surfaces this as a re-authentication prompt. There is no automatic silent refresh — offline_access is requested for future refresh token support, but is not currently used. If a token expires while you have a pending approval in flight, re-run jitsudo login and then check jitsudo status — the pending request is unaffected and preserved in the database.

Stolen token replay. A stolen but unexpired JWT can be used to submit elevation requests until it expires. jitsudo has no additional binding (e.g., mTLS client certificate binding) to tie a JWT to a specific device. Mitigations:

  • Short JWT lifetimes: Configure 60–120 minute token lifetimes in your IdP. Do not exceed 4 hours for production use. All major IdPs (Okta, Entra ID, Keycloak) support configuring access and ID token lifetimes.
  • IdP session revocation: Revoking an IdP session or account blocks new tokens from being issued. If a token is stolen, immediately revoke the IdP session to limit the window.
  • Step-up authentication: Configure your IdP to require re-authentication before issuing tokens for sensitive operations. Some IdPs support step-up auth policies tied to specific client IDs or scopes.

Revoking an IdP account or removing group memberships blocks new elevation requests immediately — the next request will fail eligibility evaluation.

However, active grants are not automatically revoked. Always run:

Terminal window
jitsudo status --user departing-engineer@example.com --state active
jitsudo revoke <each-active-request-id>

See Writing Policies — Principal Lifecycle for the full offboarding procedure.


The oidc_issuer in your config must exactly match the iss claim in the JWT. Fetch a token and inspect it:

Terminal window
# Decode the JWT (base64 decode the middle segment)
jitsudo login --provider https://your-idp.example.com
cat ~/.jitsudo/credentials | python3 -c "
import json, base64, sys
token = json.load(sys.stdin)['Token']
payload = token.split('.')[1]
# Add padding
payload += '=' * (4 - len(payload) % 4)
print(json.dumps(json.loads(base64.b64decode(payload)), indent=2))
"

Check the iss field and make sure it matches your config exactly.

If eligibility policies use input.user.groups but the claim is empty, your IdP is not including groups in the ID token. Add the groups claim as described above for your IdP.

Not all IdPs enable device flow by default. Check that:

  • The client type is Native or Public (not Web).
  • Device authorization grant is explicitly enabled.
  • Your IdP’s device authorization endpoint is reachable from the CLI machine.