This is a quick tutorial on how to configure Okta authentication.

Quick setup with Igniter

The fastest way to add Okta authentication is with the Igniter generator:

mix ash_authentication.add_strategy okta

This creates the UserIdentity resource, register action, secrets wiring, and strategy DSL for you. Follow the printed instructions to register your Okta application and set the required environment variables. The rest of this tutorial covers manual setup.

Manual setup

First you'll need a registered application in your Okta Admin Console to get your OAuth 2.0 credentials.

  1. In the Admin Console, go to Applications > Applications
  2. Click Create App Integration
  3. Choose OIDC - OpenID Connect as the sign-in method, and Web Application as the application type, then click Next
  4. Give the app a name
  5. Under Sign-in redirect URIs, add your callback URL — e.g. http://localhost:4000/auth/user/okta/callback
  6. Choose which Okta users should have access (assignments) and click Save
  7. From the app's General tab, copy the Client ID and Client secret

You'll also need your Okta domain (e.g. mycompany.okta.com) — visible in the Admin Console URL — and an authorization server. Most installations should use the built-in default Custom Authorization Server: combined, your base_url is https://mycompany.okta.com/oauth2/default.

Org vs Custom Authorization Server

Okta exposes two kinds of authorization servers:

  • Custom Authorization Server (recommended) — issuer is https://YOUR_OKTA_DOMAIN/oauth2/{authServerId}. Every Okta org ships with one named default. Configure claims, scopes, and policies under Security > API > Authorization Servers.
  • Org Authorization Server — issuer is https://YOUR_OKTA_DOMAIN. Only suitable for a small number of Okta-internal use cases.

If you're not sure, use the default Custom Authorization Server.

Next we configure our resource to use Okta credentials:

defmodule MyApp.Accounts.User do
  use Ash.Resource,
    extensions: [AshAuthentication],
    domain: MyApp.Accounts

  attributes do
    # ...
  end

  authentication do
    strategies do
      okta do
        client_id MyApp.Secrets
        client_secret MyApp.Secrets
        redirect_uri MyApp.Secrets
        base_url MyApp.Secrets
      end
    end
  end
end

Please check the guide on how to properly configure your Secrets. The base_url should resolve to something like https://mycompany.okta.com/oauth2/default.

Then we need to define the action that will handle the OIDC flow. For Okta the action is :register_with_okta — it handles both registration of new users and sign-in for existing ones.

defmodule MyApp.Accounts.User do
  require Ash.Resource.Change.Builtins
  use Ash.Resource,
    extensions: [AshAuthentication],
    domain: MyApp.Accounts

  # ...
  actions do
    create :register_with_okta do
      argument :user_info, :map, allow_nil?: false
      argument :oauth_tokens, :map, allow_nil?: false
      upsert? true
      upsert_identity :unique_email

      change AshAuthentication.GenerateTokenChange

      # Required if you have the `identity_resource` configuration enabled.
      change AshAuthentication.Strategy.OAuth2.IdentityChange

      change {AshAuthentication.Strategy.OAuth2.UserInfoToAttributes, fields: [:email]}

      # Required if you're using the password & confirmation strategies
      upsert_fields []
      change set_attribute(:confirmed_at, &DateTime.utc_now/0)
    end
  end

  # ...
end

Ensure you set hashed_password to allow_nil?: true if you are also using the password strategy:

defmodule MyApp.Accounts.User do
  # ...
  attributes do
    # ...
    attribute :hashed_password, :string, allow_nil?: true, sensitive?: true
  end
  # ...
end

Then generate and run migrations:

mix ash.codegen make_hashed_password_nullable
mix ash.migrate

Working with Okta groups

If you've configured your authorization server to include a groups claim (under Security > API > Authorization Servers > {server} > Claims), the claim will appear in the user_info argument passed to your register_with_okta action.

The shape of the value depends on the claim's configuration:

  • When the claim's value type is Groups with a "Matches regex" / "Starts with" / "Equals" filter, Okta returns a JSON array of group names.
  • When the claim's value type is Expression returning a single string, Okta returns a string.

Normalise both shapes before pattern-matching — e.g. wrap with List.wrap/1:

groups = user_info |> Map.get("groups", []) |> List.wrap()

For full user/group sync (provisioning users from Okta and keeping group membership in step), prefer SCIM over driving everything off the OIDC groups claim — the claim is only populated when a user signs in, and won't catch group changes made while the user is already authenticated.

Step-up authentication / MFA

To force re-authentication, request specific factors, or pass other Okta-specific authorization parameters, use authorization_params. The acr_values to pass depend on which Okta engine your org runs:

okta do
  # ...
  authorization_params prompt: "login"
end
# Okta Identity Engine (default for tenants created since 2022):
okta do
  # ...
  authorization_params acr_values: "urn:okta:loa:2fa:any:ifpossible"
end
# Okta Classic Engine only:
okta do
  # ...
  authorization_params acr_values: "urn:okta:loa:2fa:any"
end

See Okta's step-up authentication guide for the current ACR value catalog and engine differences.