Enterprise · Identity federation
Enterprise SSO
This page is for IT admins standing up Parleq’s LLM cleanup access behind one corporate OIDC sign-in instead of handing each user an API key. The user signs in once with their company identity provider; Parleq federates that sign-in into your cloud — AWS Bedrock and/or Google Cloud Vertex AI — and mints short-lived per-cloud credentials at call time. No long-lived cloud secret is stored on the device.
Setting this up at home without an IT department? The same engine works for a single user on a free identity provider (Cognito) or a personal Google account — with different gotchas. See the DIY: SSO & Sign-in with Google guide for the path-picker and the burn-an-hour gotchas.
What you get
One engine, several legs. The user runs a single OIDC sign-in (PKCE); Parleq trades the resulting token for short-lived cloud credentials on demand. The properties that matter for a security review:
- • No per-user API keys. Nothing to distribute, store, or rotate per employee — identity comes from your IdP.
- • Short-lived cloud credentials, memory only. Only the refresh token touches the Keychain; the cloud credentials live in process memory and expire on their own.
- • Fail-closed. If any leg fails, Parleq pastes the raw on-device transcript rather than falling back to a personal credential — the cleanup pass is skipped, the dictation is not lost.
- • A connection doctor. Settings → Company Account runs token-free discovery plus a silent refresh and reports which hop (IdP / token exchange / provider) last succeeded or failed — the place to confirm a new setup end-to-end.
- • Per-user audit attribution. The AWS leg carries the signed-in user’s email as the role session name, so CloudTrail attributes each call to the person.
- • A revocation caveat. Offboarding (disabling the user at the IdP) takes effect at the next refresh, but already-issued STS credentials are not revocable and live to their session maximum — keep the AWS session duration short if you need tight offboarding.
How it works
Parleq runs an OIDC authorization-code flow with PKCE in a system web view (no embedded browser, no password ever touches Parleq). The flow yields a refresh token, which Parleq stores in the macOS Keychain and uses to mint short-lived ID tokens on demand. Each cloud leg then trades that ID token for its own credentials:
- • AWS Bedrock —
AssumeRoleWithWebIdentityexchanges the ID token for temporary STS credentials scoped to an IAM role you control. - • Google Cloud Vertex AI — Workforce Identity Federation exchanges the ID token for a federated access token used directly as the Vertex bearer.
Two principles bound the data handling:
- • What crosses which boundary. Only OIDC tokens reach the identity provider and the cloud STS endpoints (
sts.<region>.amazonaws.com,sts.googleapis.com) — never transcript text. Transcript text only ever reaches the cleanup LLM endpoint you already configured, exactly as with any other auth mode. - • Fail-closed. If sign-in, refresh, or the per-cloud exchange fails, Parleq does not silently fall back to a personal credential or a different provider. It fails closed and pastes the raw on-device ASR transcript — the cleanup pass is skipped, but your dictation is never lost.
Refresh tokens are rotated where the IdP supports it: Parleq persists each newly-issued refresh token to the Keychain the instant it arrives, before doing anything else, so a rotated token is never lost. The access and ID tokens stay in process memory only. Offboarding (disabling the user at the IdP) takes effect at Parleq’s next token refresh; note that already-issued STS credentials are not revocable and live to their session maximum, so keep the AWS session duration short if you need tight offboarding (see the managed-configuration reference and security review).
Azure OpenAI note: the Azure OpenAI provider keeps its existing Microsoft Entra ID auth (via az login). OIDC federation for Azure is not part of this release — the two federated legs today are AWS Bedrock and Google Cloud Vertex AI; Azure OpenAI keeps its existing Entra ID and API-key auth modes.
Enabling it in Parleq
Set the shared OIDC section, then turn on whichever cloud leg(s) you need. All of these can be pinned fleet-wide via MDM — see the Enterprise OIDC federation keys. Hand-editing ~/.parleq/config.json directly:
{
"oidc": {
"issuer": "https://acme.okta.com",
"client_id": "0oaEXAMPLEclientid",
"scopes": ["openid", "profile", "email", "offline_access"],
"ephemeral_browser": false
},
"aws": {
"region": "us-east-1",
"auth_mode": "oidc",
"role_arn": "arn:aws:iam::123456789012:role/ParleqBedrock",
"session_duration_seconds": 3600
},
"vertex": {
"project": "my-gcp-project",
"region": "us-central1",
"auth_mode": "oidcFederation",
"workforce_provider": "locations/global/workforcePools/parleq-pool/providers/oidc"
}
} Then open Settings → Company Account and sign in. That section shows the signed-in identity and a connection doctor that runs token-free discovery plus a silent refresh and reports each cloud leg’s last success/failure — the place to confirm a new IdP or cloud setup end-to-end.
Identity-provider playbooks
Okta
Register Parleq as a native / public application (authorization-code + PKCE, no client secret):
- • Application type: Native. Grant types: Authorization Code and Refresh Token. PKCE: required.
- • Sign-in redirect URI:
parleq-auth://oidc/callback. - • Scopes:
openid profile email offline_access(offline_accessis what gets you a refresh token). - • Issuer: your org / custom authorization-server URL (e.g.
https://acme.okta.com). Client ID: the app’s client ID.
Refresh rotation: Okta rotates refresh tokens and runs replay detection — reusing an old refresh token after rotation revokes the session. Parleq handles this by persisting the rotated token on receipt, but it means a lost rotated token forces a fresh sign-in. Because Okta is a public issuer with a publicly-fetchable JWKS, it works for both the AWS and GCP legs.
Keycloak
Create a public client in your realm with the standard (authorization-code) flow enabled and PKCE (S256) required:
- • Client authentication: off (public client). Standard flow: on. PKCE method:
S256. - • Valid redirect URI:
parleq-auth://oidc/callback. - • Realm refresh rotation:
revokeRefreshTokenon withrefreshTokenMaxReuse0 to rotate on every refresh. - • Issuer:
https://<host>/realms/<realm>.
A ready-made dev rig — docker-compose Keycloak with this exact client and a test user — lives in the repo at scripts/dev/keycloak/. One caveat for end-to-end cloud testing: AWS STS fetches the issuer’s JWKS from its own side, so a localhost issuer can’t pass AssumeRoleWithWebIdentity. GCP accepts an inline-uploaded JWKS (no fetch), but requires the issuer URI itself to use HTTPS — so a plain-http localhost issuer works for neither cloud leg. For live cloud tests, use a hosted free issuer (Cognito or an Okta Integrator org) or front Keycloak with a stable-hostname HTTPS tunnel.
Generic OIDC
Any spec-compliant OIDC provider works if it offers:
- • A discovery document at
<issuer>/.well-known/openid-configurationwithauthorization_endpointandtoken_endpoint(arevocation_endpointenables clean sign-out). - • A public client (no secret) supporting authorization-code + PKCE (S256).
- • The custom-scheme redirect URI
parleq-auth://oidc/callback. - • Refresh tokens (request
offline_access) so Parleq can refresh silently. - • An HTTPS issuer URI; the AWS leg additionally needs the JWKS publicly fetchable (GCP can take an inline-uploaded JWKS instead).
AWS Bedrock setup
In IAM, register the IdP as an OpenID Connect identity provider (the issuer URL plus the client ID as an audience), then create a role whose trust policy admits tokens from that provider. AssumeRoleWithWebIdentity is the gate: the call itself is unauthenticated, so the role’s trust policy is what authorizes access — constrain it with an aud condition.
Example role trust policy (placeholder account ID and client ID):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/acme.okta.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"acme.okta.com:aud": "0oaEXAMPLEclientid"
}
}
}
]
} Attach a permissions policy granting Bedrock model invocation:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
"Resource": "*"
}
]
} "Resource": "*" keeps the example short — scope it to the specific model or inference-profile ARNs you have approved for least privilege.
Put the role’s ARN in aws.role_arn and set aws.auth_mode to oidc. The requested STS session duration is aws.session_duration_seconds (default 3600, clamped to 900–43200). Bedrock model access is per-region, so make sure the chosen region has the model enabled. The role’s session name carries the signed-in user’s email for CloudTrail attribution.
Google Cloud Vertex AI setup
Create a Workforce Identity Pool and add an OIDC provider for your IdP (issuer URL + the client ID as the allowed audience). For a publicly-reachable issuer GCP fetches the JWKS automatically; for a non-public issuer you can upload the JWKS inline (jwks_json) — but note the issuer URI must use HTTPS either way. Workforce Identity Pools are an Organization-level resource — creating one requires a GCP Organization (Cloud Identity) and iam.workforcePools.create, not just project access.
- • Attribute mapping:
google.subject = assertion.sub. - • Grant access: bind
roles/aiplatform.userto the workforce-pool principal set so federated identities can call Vertex AI. Scope the binding tighter than the whole pool if you can (e.g. by group attribute). - • Workforce provider name: put the full resource name in
vertex.workforce_provider(locations/global/workforcePools/<pool>/providers/<provider>) and setvertex.auth_modetooidcFederation.
Workforce-federated calls must specify a billing/quota project: Parleq sends your configured vertex.project as the x-goog-user-project header on every Vertex request, so that project needs the Vertex AI API enabled and the federated principal needs serviceusage.services.use on it.
Google account → Vertex AI directly
The third leg skips federation entirely. GCP refuses to accept Google itself as a workforce IdP, so the workforce-pool path can’t do “sign in with Google, dictate with Gemini.” Instead, Parleq’s own Google sign-in requests the cloud-platform scope, and the resulting OAuth access token is a valid Vertex bearer directly — exactly what gcloud Application Default Credentials produces, done in-app, with no broker and no gcloud install. This is the googleOAuth Vertex mode.
- • In the Google Cloud console, create an OAuth client ID of the iOS type (public, no secret; uses the reversed-client-ID redirect scheme). Set
oidc.redirect_uritocom.googleusercontent.apps.<your-client-id>:/oauth2redirect. - • Scopes:
["openid", "email", "https://www.googleapis.com/auth/cloud-platform"]— thecloud-platformscope is what makes the access token usable against Vertex. - • Force a refresh token: set
oidc.extra_auth_paramsto{"access_type": "offline", "prompt": "select_account consent"}. Google only re-issues a refresh token when it shows the consent screen. - • Set
vertex.auth_modetogoogleOAuthandvertex.projectto the GCP project that owns the Vertex AI quota. Every call carriesx-goog-user-project, so the signing account needs bothroles/aiplatform.userandroles/serviceusage.serviceUsageConsumeron it (a project Owner already has both).
For org-owned Google identities this works the same way — with no “unverified app” warning and no refresh-token cap if the OAuth client lives in your Google Cloud organization. The personal-@gmail variant, with its consent-screen trade-offs and the granular-consent gotcha, is covered in the DIY guide.
Pin it fleet-wide
Every value above can be pushed via MDM so users sign in but can’t re-point the app at a personal tenant. The nine enterprise OIDC keys — oidcIssuer, oidcClientID, oidcScopes, oidcRedirectURI, oidcExtraAuthParams, oidcEphemeralBrowserSession, awsRoleArn, awsSessionDurationSeconds, and vertexWorkforceProvider — sit alongside the existing managed-config surface. Enable a leg by also pinning awsAuthMode: oidc and/or vertexAuthMode: oidcFederation (for Workforce federation) or vertexAuthMode: googleOAuth (for the Sign-in-with-Google direct leg). See the Managed Configuration reference → Enterprise OIDC federation for the full table, and the Admin Guide for the deployment workflow.
Troubleshooting
Start at Settings → Company Account → Test connection. The connection doctor reports which hop failed (IdP discovery / token exchange / cloud provider) with the server’s reason, which usually points straight at the fix below.
| Symptom | Cause / fix |
|---|---|
tokenCacheNotFound / SSO token error on AWS | If your IAM OIDC provider URL or the IdP issuer ends in /#, drop the trailing # — the INI parser strips it mid-value. Confirm the issuer in the doctor matches the IAM OIDC provider exactly. |
AssumeRoleWithWebIdentity rejected (audience / not authorized) | The role’s trust policy aud condition must equal the OIDC client ID, and the Federated principal must name the same issuer host you registered. The STS call is unauthenticated — the trust policy is the only gate. |
| AWS leg fails with a JWKS / issuer error on a self-hosted IdP | STS fetches the issuer’s JWKS server-side, so a localhost issuer cannot pass. Front the IdP with a stable-hostname HTTPS URL (or use a hosted issuer such as Cognito). |
| GCP workforce provider create fails: “issuer must be HTTPS” | GCP can take an inline-uploaded JWKS (no fetch), but the issuer URI itself must still be HTTPS — a plain-http localhost issuer works for neither cloud leg. |
Sign-in window flashes open and closes (invalid_scope) | The IdP rejected a requested scope the client doesn’t have enabled. On a Cognito app client this is usually profile — request only ["openid", "email"] or enable Profile in the app client. Cognito also rejects offline_access. |
| Signed out shortly after signing in (refresh rejected) | A rotated refresh token was lost, or the IdP enforces replay detection. Sign in again; for IdPs that rotate on every refresh, ensure nothing else is reusing the token. |