Skip to main content

Okta SSO for AlmaForge

AlmaForge authenticates users against Okta through the standard OIDC flow. Each AlmaForge role is mapped to an Okta group. Adding a user to the group grants them the role on their next sign-in. Removal also takes effect at the next sign-in, so lock the user when access must end immediately. AlmaForge does not keep passwords for SSO users.

The integration is built into almad. Configure it with an OIDCConnector resource. See the generic OIDC guide for the full field list and the shared verification, high-availability, and break-glass content. This page covers only the Okta-specific clicks.

This guide uses an Okta Custom Authorization Server to define the groups scope, claims, and access policy in one place. Okta's org authorization server also supports a groups claim in ID tokens. For that alternative, configure the claim on the app using Okta's groups guide and use the org issuer URL in the generic OIDC setup.

TL;DR​

  1. Create an OIDC application in Okta and capture the client ID and secret (Step 1).
  2. Create a custom authorization server and note its Issuer URI (Step 2).
  3. Add a groups scope and claim to the authorization server (Step 3).
  4. Add an access policy and rule so the auth server will issue tokens (Step 4).
  5. Apply the OIDCConnector from Step 5.
  6. Verify with a real login (Step 6).

Limitations​

Test recovery before changing SSO. AlmaForge has no local accounts. If this is the only connector, a failure prevents new logins. Review SSO-first behavior on the OIDC page, and confirm your break-glass procedure works before running apply on a production cluster.

This guide uses a custom authorization server. Confirm that your Okta organization has API Access Management before following the custom-server steps. The org-server alternative above supports group-based sign-in without those steps.

The access policy is a non-obvious setup gotcha. Custom authorization servers do not ship with a default access policy. Without an explicit policy and rule on the auth server, Okta returns access_denied at token-exchange time even though the user's browser sign-in appeared to succeed. See Step 4 for the required policy shape.

Okta rate limits. Okta enforces strict per-org rate limits on the token and userinfo endpoints. A multi-node AlmaForge auth cluster with every node talking to Okta independently can hit those limits during a large login spike. Monitor for 429 Too Many Requests in the auth journal and contact Okta support for a rate-limit uplift if this becomes a real problem.

Prefer group assignment over "Allow everyone" on the app. Assign the app to the groups that should use AlmaForge. The IdP app assignment and the connector's claimsToRoles mapping are both required: AlmaForge rejects a login when no mapping grants a role.

Keep credentials outside the manifest. clientSecret can load its value from a file or environment variable. See Secret storage.

Prerequisites​

Before you start, make sure you have:

  • A running AlmaForge cluster with an Auth Service that you can reach over the network and a working alma CLI pointed at it.
  • An Okta organization and an admin account that can create OIDC applications and edit authorization servers. The default Super Admin role covers this.
  • Okta's API Access Management feature enabled. Required for custom authorization servers.
  • One or more Okta groups that you will map to AlmaForge roles.
  • The public address of your AlmaForge Proxy Service, for example almaforge.example.com.
  • Outbound HTTPS egress from almad to your Okta domain (for example example.okta.com) from every auth host.
  • A break-glass procedure in place and tested.

Step 1. Create an OIDC application​

In the Okta admin console, open Applications → Applications → Create App Integration. Select:

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

Click Next and configure:

  • App integration name: something recognizable, for example AlmaForge.
  • Grant type: enable Authorization Code. AlmaForge does not request refresh tokens for this sign-in flow.
  • Sign-in redirect URIs: https://almaforge.example.com/api/v1/oidc/callback, substituting your own proxy host.
  • Sign-out redirect URIs: optional, typically https://almaforge.example.com/.
  • Controlled access: assign the app to specific Okta groups rather than leaving it as Allow everyone. See the warning below.

Click Save.

Prefer specific group assignment over "Allow everyone"

Assign the app only to the intended users or groups. AlmaForge also requires at least one matching role mapping before it creates the user session. Test both app assignment and role mapping with an account from each intended group.

On the application General tab, copy these two values:

  • Client ID.
  • Client secret (click the eye icon to reveal).
Treat the client secret as a password

Store the client secret in the same secret manager you use for production credentials. Do not commit it to Git.

Step 2. Create a custom authorization server​

In the Okta admin console, open Security → API → Authorization Servers → Add Authorization Server and configure:

  • Name: almaforge (or any label).
  • Audience: almaforge (any string will do). Okta requires a value and echoes it back in the token's aud claim. The go-oidc library that AlmaForge uses to verify ID tokens automatically validates aud against the configured clientID, so you do not need to match the audience string anywhere in the connector.
  • Description: optional.

Click Save. On the new authorization server's Settings tab, copy the Issuer URI. It will look like https://<okta-domain>/oauth2/<auth-server-id>. This is the value you will put into spec.issuerURL in the connector YAML.

Step 3. Add a groups scope and claim​

On the authorization server, open the Scopes tab and click Add Scope. Configure:

  • Name: groups.
  • Display name: Groups.
  • Description: Return the user's Okta groups.
  • Leave the defaults for everything else.

Switch to the Claims tab and click Add Claim. Configure:

  • Name: groups.
  • Include in token type: ID Token, Always.
  • Value type: Groups.
  • Filter: Matches regex with value .* to include every group the user is a member of. Use Starts with or Equals to limit the emitted groups to a specific prefix or name.
  • Include in: The following scopes: groups.

Save the claim.

Step 4. Update the authorization server access policy​

On the authorization server, open the Access Policies tab. Okta creates a default policy for the built-in authorization servers but not for custom ones. You need to add one before tokens will be issued.

Click Add Policy and fill in:

  • Name: Default.
  • Assign to: The following clients, then pick the OIDC application you created in Step 1.

Save the policy. Inside the policy, click Add Rule and configure:

  • Rule name: Default.
  • IF Grant type is: check Authorization Code.
  • AND User is: Any user assigned the app.
  • AND Scopes requested: Any scopes.

Save the rule.

No policy means access_denied at token exchange

Without this policy and rule on the custom authorization server, Okta issues an access_denied error at token-exchange time even though the browser sign-in appears to succeed. This is the most common first-time setup failure. If you skip this step you will debug a confusing error whose only fix is to come back and finish it.

Step 5. Apply the OIDCConnector​

Write a YAML file with the values you collected:

okta.yaml:

examples/sso/okta.yaml
# Okta connector using a custom authorization server and group claims.
apiVersion: almaforge.com/v1
kind: OIDCConnector
metadata:
name: okta
spec:
display: Okta
issuerURL: https://<okta-domain>/oauth2/<auth-server-id>
clientID: <client-id>
clientSecret: <client-secret>
scope:
- email
- profile
- groups
claimsToRoles:
- claim: groups
value: almaforge-admins
roles: [admin, access]
- claim: groups
value: almaforge-developers
roles: [access]

Replace:

  • <okta-domain> and <auth-server-id> with the Issuer URI from Step 2. For example https://example.okta.com/oauth2/aus1abc2defGHIjk34.
  • <client-id> and <client-secret> with the values from Step 1.
  • The claimsToRoles entries with your own Okta group names and AlmaForge roles. Group matching uses the Okta group name, not a slug or a UUID. Copy it verbatim from the Okta Directory → Groups page.

The scope list must include groups explicitly. Otherwise Okta will not emit the claim even though you defined it in Step 3. email and profile are listed here so AlmaForge can populate the username from the email claim.

Test break-glass before applying

AlmaForge has no local accounts. If this is the only connector, a wrong claim mapping or expired client secret prevents new logins. Confirm your break-glass procedure works before applying the connector to a production cluster.

Apply the connector:

Terminal
alma apply -f okta.yaml

Step 6. Verify the connector​

Open the cluster's audit log for your test account. See SSO verification for event fields and the distinction from service logs.

Sign out and log in through Okta:

Terminal
alma logoutalma login --proxy=almaforge.example.com

A browser opens the cluster's sign-in page. Choose Okta. A successful first-time login emits a UserLogin event and the CertificateCreate event for the certificate it mints:

JSON
{
"kind": "AuditEvent",
"apiVersion": "almaforge.com/v1",
"spec": {
"id": "5a4b3c2d-1e0f-4958-8776-655443322110",
"index": 0,
"timestamp": "2026-03-30T15:58:58.999000Z",
"clusterName": "almaforge.example.com",
"code": "T1001I",
"type": "UserLogin",
"verb": "login",
"actor": {
"name": "alice@example.com",
"type": "user"
},
"status": {
"success": true
},
"userLogin": {
"method": "oidc",
"attributes": {
"connector_id": "okta",
"connector_type": "oidc",
"email": "alice@example.com",
"name": "Alice Example",
"request_id": "8f0c1e6a-2f7e-4a7a-9f1e-1f0b6d0f1a2b",
"username": "alice@example.com"
}
}
}
}
JSON
{
"kind": "AuditEvent",
"apiVersion": "almaforge.com/v1",
"spec": {
"id": "b1c2d3e4-f506-4718-8929-3a4b5c6d7e8f",
"index": 0,
"timestamp": "2026-03-30T15:58:58.999000Z",
"clusterName": "almaforge.example.com",
"code": "TC000I",
"type": "CertificateCreate",
"verb": "create",
"certificateCreate": {
"certType": "user",
"identity": {
"user": "alice@example.com",
"roles": [
"admin"
],
"logins": [
"tester",
"ec2-user",
"root"
],
"traits": {
"email": [
"alice@example.com"
],
"groups": [
"almaforge-admins",
"engineering"
],
"name": [
"Alice Example"
],
"preferred_username": [
"alice@example.com"
]
}
}
}
}
}

The field to check is the groups trait on the CertificateCreate event. It contains the group names Okta emitted on the groups claim, filtered by the regex you configured in Step 3. Each value in your claimsToRoles.value must match one of these entries verbatim. Matching is case-sensitive. If the groups trait is missing or empty:

  • The groups scope is not listed in spec.scope on the connector (Step 5).
  • The groups claim is not configured on the custom authorization server (Step 3), or the filter excludes every group the user is in.
  • The user is not assigned to the OIDC application in Okta (Step 1's Controlled access).

After a successful login, you can also run alma get user/<username> -o yaml and inspect spec.traits.groups.

Read the full audit-event breakdown on the generic OIDC verification section.

Troubleshooting​

access_denied after the callback​

Okta returned access_denied or invalid_grant at token exchange, even though the browser sign-in appeared to succeed. The custom authorization server has no access policy and rule. Open Security → API → Authorization Servers → <your-server> → Access Policies and add them. See Step 4.

No roles match the login​

The login is rejected if no mapping assigns a role. Either the groups claim is missing from the token (Step 3 was skipped, or the groups scope was not added to the connector's spec.scope in Step 5), or the claim contains group names that do not match claimsToRoles.value. Set ALMA_LOG_LEVEL=debug on the Auth host, sign in again, and copy the group name straight out of the claims Okta sent:

Terminal
journalctl -u almad -f | grep 'Extracted claims'

If the login succeeds but the stored identity still looks wrong, inspect the user record as described in Claims, traits, and role templates.

Invalid issuer URL​

For this guide's custom-server setup, use https://<okta-domain>/oauth2/<auth-server-id> as issuerURL. Copy it from the authorization server's Settings tab. The org issuer https://<okta-domain> applies only when you configured the alternative org-server flow.

unauthorized_client​

The client ID in the connector belongs to a different OIDC application than the one assigned to the authorization server's access policy, or the user is not assigned to the app at all. Re-check both and re-apply.

429 Too Many Requests​

Okta returned 429 Too Many Requests on the token or userinfo endpoint. Okta enforces strict per-org rate limits. In a multi-node auth cluster during a login spike, every auth node hits Okta independently. Confirm the volume is legitimate and, if it is, contact Okta support for a rate-limit uplift.

Operations​

Lifecycle​

Apply connector changes before testing the next login. No almad restart is needed. See When connector changes take effect for discovery caching and IdP-side changes.

High availability​

Every auth node talks to Okta independently. A multi-node auth cluster must have working egress to your Okta domain from every auth host. Each auth instance also caches the OIDC discovery document per-instance for one hour. See High availability on the generic OIDC page for the full list of per-instance assumptions.

Egress requirements​

Auth hosts require outbound TCP/443 to your Okta domain (for example example.okta.com). No inbound connectivity from Okta is required, and the integration does not open any listening sockets.

Client secret rotation​

Okta lets you generate a new client secret on the OIDC application's General tab. The procedure is:

  1. On the application General tab, click Generate new client secret. Do not delete the old one yet.
  2. Copy the new secret value.
  3. Run alma apply -f okta.yaml with the new value.
  4. Run a test login through the connector to confirm the new secret works.
  5. Delete the old secret on the Okta app.

The connector uses one client secret at a time. Keep the previous secret valid until test logins succeed from every auth host, then retire it in Okta.

Secret storage​

Load clientSecret from a file or environment variable using Secret storage, which also covers protection of inline credentials and backups.

Break-glass access​

Test break-glass access before relying on this connector. Okta-specific recovery cases include a deleted authorization server or revoked access policy, alongside rotated client secrets and incorrect claimsToRoles mappings.

Monitoring​

Monitor the audit log for login outcomes. For diagnostics, inspect the Auth journal for token validation and the Proxy journal for browser callbacks. On each service host, tail OIDC-related messages with:

Terminal
journalctl -u almad -f | grep -Ei 'okta|oidc'

Check UserLogin failures and their spec.status.error field. See SSO verification for event codes, certificate fields, and debug-only journal copies. Okta rate-limit errors include the HTTP status and Okta's description in the Auth journal. Collect records from audit event storage for SIEM ingestion. Collect service diagnostics from the journal separately.

Removal​

Delete the connector resource:

Terminal
alma delete oidcconnector/okta

Existing sessions issued through the connector remain valid until their certificates expire. No new sessions will be issued.

On the Okta side, you can also delete the OIDC application and the custom authorization server. If other AlmaForge clusters share the same authorization server, leave it in place.

References​