Skip to content

Okta Integration

Shoehorn supports Okta as both an authentication provider (OIDC) and an orgdata provider (Okta Management API for user/team sync). This guide walks through the exact Okta app configuration, Helm values, and troubleshooting mapped to the error codes Shoehorn emits on /auth-error.

  • An Okta organization with admin access
  • Shoehorn deployed via the Helm chart — see Helm deployment
  • Your Shoehorn domain (e.g. shoehorn.example.com) reachable from browsers that will sign in

In the Okta admin console, go to Applications → Create App Integration and select:

  • Sign-in method: OIDC – OpenID Connect
  • Application type: Web Application

Then configure the following fields exactly — the non-default values matter:

FieldValue
App nameShoehorn
Client authenticationClient secret
Proof of possession (PKCE)Require PKCE as additional verification
Client secretsGenerate one and copy it — you will store this as OKTA_CLIENT_SECRET
Grant typeAuthorization Code    ☑ Refresh Token
Refresh token behaviorRotate token after every use
Sign-in redirect URIshttps://<your-domain>/api/v1/auth/callback
Sign-out redirect URIshttps://<your-domain>
Login initiated byApp Only
Controlled accessAssign to the Okta groups that should have Shoehorn access

Save, then copy the Client ID — you will store this as OKTA_CLIENT_ID.

If users cannot sign in after completing the rest of this guide, check Applications → Shoehorn → Sign On → Sign On Policy. A permissive starting rule is:

  • Catch-all rule: Access = Allowed, Authentication = Any 2 factor types

Tighten this once signing in works end-to-end.

Okta does not include group membership in the ID token by default. Without this claim, Shoehorn cannot map your Okta groups to Shoehorn roles — logins will land on /auth-error?code=GROUPS_CLAIM_MISSING.

  1. Security → API → Authorization Servers — select the default authorization server (or your custom one)
  2. Claims tab → Add Claim
FieldValue
Namegroups
Include in token typeID Token, Always
Value typeGroups
FilterMatches regex, value .*
Include inAny scope

Save. Use Okta’s Token Preview (same authorization server page) to confirm the token now carries a groups array.

Required for production. Without it, every login fails with INVALID_CLAIMS and the user lands on /auth-error. Shoehorn uses this claim as the tenant id for every request; if it’s missing, the request is rejected.

  1. Go to Security → API → Authorization Servers and select the same authorization server you used for the groups claim
  2. Open the Claims tab and click Add Claim
FieldValue
Nameokta_org
Include in token typeID Token, Always
Value typeExpression
ValueYour Okta org id, or an expression that returns it. A literal like "acme" works for a single-tenant deploy. For per-user attribution use something like user.organization.
Include inAny scope

The value is an opaque tenant id, so pick something stable. Once a user has signed in with one value, switching to another would orphan their existing data.

Use Okta’s Token Preview to confirm the ID token now carries okta_org.

4. (Optional) Create an API Token for Orgdata Sync

Section titled “4. (Optional) Create an API Token for Orgdata Sync”

If you want Shoehorn to sync users and teams proactively from Okta — rather than only resolving memberships on individual logins:

  1. Security → API → TokensCreate Token
  2. Name: Shoehorn Orgdata Sync
  3. Copy the token — you will store this as OKTA_API_TOKEN

The token runs as the creating user. If that user loses read access to users or groups, sync breaks silently — prefer creating the token as a dedicated service account.

A working reference is in examples/values-okta.yaml. Minimum configuration:

global:
domain: shoehorn.example.com # used for the callback-URI allowlist
auth:
provider: okta
okta:
# From step 1 — no scheme, no trailing slash
domain: your-org.okta.com
clientId: 0oaXXXXXXXXXXXXXX
# Optional — enable orgdata sync from step 3
orgdata:
enabled: true
providers: ["okta"]
primaryProvider: "okta"
# Bootstrap admin access before UI-side mappings exist
adminAssignment:
adminGroups: "shoehorn-admins"
secret:
existingSecret: shoehorn-auth-secrets
mappings:
OKTA_CLIENT_SECRET: okta_client_secret
OKTA_API_TOKEN: okta_api_token # only if orgdata is enabled
SESSION_ENCRYPTION_KEY: session_encryption_key
JWT_SECRET: jwt_secret
AUTH_ENCRYPTION_KEY: auth_encryption_key

Create the Kubernetes secret before installing:

Terminal window
kubectl create secret generic shoehorn-auth-secrets \
--from-literal=okta_client_secret='<client-secret>' \
--from-literal=okta_api_token='<api-token>' \
--from-literal=session_encryption_key="$(openssl rand -hex 32)" \
--from-literal=jwt_secret="$(openssl rand -hex 32)" \
--from-literal=auth_encryption_key="$(openssl rand -hex 32)"

The chart’s schema enforces that auth.okta.domain and auth.okta.clientId are set whenever auth.provider: oktahelm install will refuse with a minLength: got 0, want 1 error if either is missing.

PathRequiredDescription
auth.provideryesMust be okta
auth.okta.domainyesOkta org domain, e.g. your-org.okta.com. No https://, no trailing slash
auth.okta.clientIdyesClient ID from step 1
auth.okta.issuernoIssuer URL override; leave empty for the default
auth.orgdata.enablednoSet true to sync users/teams from Okta
auth.orgdata.providersif orgdataInclude "okta"
auth.orgdata.primaryProviderif orgdata"okta" when Okta is the sole provider
auth.adminAssignment.adminGroupsrecommendedOkta groups to bootstrap as tenant admins before UI-side mappings exist
secret.mappings.OKTA_CLIENT_SECRETyesKey in the referenced secret holding the client secret
secret.mappings.OKTA_API_TOKENif orgdataKey holding the Management API token
global.domainyesUsed to build the redirect-URI allowlist

Once installed, a browser login flows end-to-end:

  1. User visits https://shoehorn.example.com → Shoehorn redirects to Okta
  2. User authenticates in Okta → Okta redirects back to /api/v1/auth/callback with an ID token
  3. Shoehorn verifies the ID token signature against Okta’s JWKS (fetched from auth.okta.domain), then validates issuer and audience
  4. If the user doesn’t exist in Shoehorn, it JIT-provisions them using sub, email, and groups claims
  5. Shoehorn resolves roles:
    • First it checks auth.adminAssignment.adminGroups against the user’s Okta groups (bootstrap path)
    • Then it applies Shoehorn-side group → role mappings configured under Admin → Directory → Teams
  6. If auth.orgdata.enabled, a targeted sync fires for the new user so team-based mappings resolve immediately rather than waiting for the next scheduled sync

Logout follows the OIDC RP-initiated logout spec:

  1. User clicks Sign out → browser hits /api/v1/auth/logout
  2. Shoehorn clears its session, then discovers Okta’s end_session_endpoint and redirects the browser there
  3. Okta clears its own session and redirects back to your Shoehorn domain
  4. The user lands signed-out; re-opening Shoehorn prompts Okta login afresh

When login fails, Shoehorn redirects to /auth-error?code=<CODE>. The code tells you where to look:

Symptom / codeLikely causeFix
code=GROUPS_CLAIM_MISSINGGroups claim not configured on the authorization serverRepeat step 2. Use Okta’s Token Preview to confirm the claim appears.
code=INVALID_CLAIMSokta_org claim missing or emptyRepeat step 3. Use Token Preview to confirm okta_org is set.
code=NO_ROLESUser has Okta groups, but none map to a Shoehorn roleConfigure a mapping under Admin → Directory → Teams, or add the user’s Okta group to auth.adminAssignment.adminGroups.
code=INVALID_DOMAINOKTA_DOMAIN is the full URL, not the bare domain, or the token endpoint is unreachableUse your-org.okta.com, no scheme, no slash. Check your cluster’s egress network path to Okta.
code=JWKS_UNAVAILABLEShoehorn couldn’t fetch Okta’s signing keysUsually transient. If it persists, verify OKTA_DOMAIN and your cluster’s egress network path to Okta.
Login redirects back to Okta in a loopSession cookie expiring at epoch 0 (very old Shoehorn build)Upgrade to Shoehorn ≥ v0.4.3.
Test Connection fails with “invalid API token”Token was revoked or the creating user was deactivatedRecreate the token as a dedicated service account.
Test Connection fails with “host unreachable”OKTA_DOMAIN contains the full URLUse your-org.okta.com, no scheme, no slash.
After logout, user is auto-signed back inend_session_endpoint not called — misconfigured appConfirm Sign-out redirect URIs in the Okta app includes https://<your-domain>.

When debugging claim issues, paste the ID token into jwt.ms and check:

  • iss matches https://<domain> (or https://<domain>/oauth2/<authz-server-id> if you use a custom authorization server)
  • aud equals your OKTA_CLIENT_ID
  • groups contains the expected array
  • sub is the stable Okta user ID (e.g. 00uXXXXXXXXXXXXXX)

Once deployed, visit Admin → Integrations → External → Okta in Shoehorn. The Test Connection button calls the Okta Management API with your stored token and returns a green banner on success or the exact upstream error on failure. It’s the fastest way to verify OKTA_API_TOKEN before investigating deeper.

Shoehorn lets you mix auth and orgdata providers:

AuthenticationOrgdataSupported
OktaOkta✅ Most common
OktaZitadel
ZitadelOkta
Oktanone (orgdata disabled)✅ Relies on adminAssignment + in-app role assignment