Skip to content

MFA Selector Script

Overview

The MFA Selector Script lets you dynamically choose the MFA requirement for each login, based on the user's identity, group membership, token claims, or request context. It runs before the MFA challenge, after any session checks.

The script works for both OIDC and SAML 2.0 applications. It is not available for external link applications.

The script returns a policy value that acts as a floor: it can only make MFA requirements stricter than the application's static mfa_policy, never looser.


Script contract

Scripts must declare package claim and export a single entry point:

package claim

func Evaluate() (string, error)

Return values:

Value Behaviour
"disabled" Skip MFA entirely (still floored by static policy)
"optional" MFA only if the user is already enrolled
"any" Require any enrolled MFA method
"otp" Require TOTP specifically
"passkey" Require a hardware/passkey authenticator

A non-nil error is treated as a script failure (see Fail-open / fail-secure).

Optional: per-application re-verification interval

If the script also defines:

func ReauthInterval() (int, error)

JustIAM calls it after Evaluate() and uses the returned value (in minutes) as the MFA re-verification interval for this application, overriding the global MFA Re-verification Interval setting in Settings → MFA. Return 0 to fall back to the global setting. If the function is absent, the global setting is used unchanged.

When both the global setting and the per-app interval are non-zero, the more restrictive (lower) value wins.


Floor behaviour

The effective MFA policy is always mostRestrictive(static_policy, script_policy):

Static policy Script returns Effective policy
optional passkey passkey ✓ (upgraded)
otp disabled otp ✓ (floor applied)
passkey any passkey ✓ (floor applied)
disabled passkey passkey ✓ (script upgrades)

Available packages

Package Functions
user user.ID(), user.Email(), user.Username(), user.FirstName(), user.LastName(), user.Groups(), user.Attributes(), user.AppMappings(), user.Claims(), user.ClaimStrings(key)
request request.LoginIP(), request.Referer(), request.TokenIP()
fmt, strings, encoding/json, net/http Safe standard library subset

user.Claims() vs user.ClaimStrings(key)

  • user.Claims() returns map[string]interface{} — the fully-resolved OIDC claims, including values produced by Application Mapping policies. Use this for non-string claims.
  • user.ClaimStrings(key) returns []string — coerces the named claim to a string slice, handling both []string and JSON-decoded []interface{} values. Use this for multi-valued claims like groups or roles.

Configuration

Field Default Description
mfa_selector_script "" Go source code for the script. Empty string disables the hook.
mfa_selector_fail_open false When true, a script error falls back to the static mfa_policy. When false (fail-secure), a script error requires any MFA method.

Adding a script

UI: Administration → Applications → (select app) → MFA Selector Script section → paste script → Save

Available for both OIDC and SAML 2.0 applications. Not shown for external link applications.

API:

PUT /api/v1/applications/{id}
{
  "mfa_selector_script": "package claim\n\nfunc Evaluate() (string, error) {\n\treturn \"any\", nil\n}",
  "mfa_selector_fail_open": false
}

Simulating the script (dry-run)

The simulate endpoint evaluates the script for a given user and returns the resolved policies without triggering any MFA challenge:

POST /api/v1/applications/{id}/simulate-mfa
Authorization: Bearer <admin-token>
Content-Type: application/json

{ "user_id": "<user-uuid>" }

Response:

{
  "static_policy":      "optional",
  "script_policy":      "passkey",
  "effective_policy":   "passkey",
  "reauth_interval_mins": 15,
  "script_error":       ""
}
Field Description
static_policy The resolved static mfa_policy for the application
script_policy The value returned by Evaluate() (omitted if no script)
effective_policy The final policy after applying the floor: mostRestrictive(static, script)
reauth_interval_mins The value returned by ReauthInterval() (omitted when 0 or no script)
script_error Non-empty when the script returned an error

Requires apps.view permission.


Fail-open / fail-secure

mfa_selector_fail_open Script error behaviour
false (default) Fail-secure: effective policy = mostRestrictive(static_policy, "any")
true Fail-open: effective policy = static mfa_policy

Required permissions

Permission Effect
apps.scripts.view The MFA Selector Script section is visible in read-only mode
apps.scripts.update Can write, modify, or clear the script

Example: require passkey for administrators

This script requires passkey authentication for admin users and also sets a shorter MFA re-verification interval for them:

package claim

import (
    "strings"
    "user"
)

const adminClaimValue = "Administrator"

var adminGroups = []string{
    "administrators",
    "platform-admins",
}

func isAdmin() bool {
    for _, g := range user.ClaimStrings("groups") {
        if strings.EqualFold(g, adminClaimValue) {
            return true
        }
    }
    for _, g := range user.Groups() {
        for _, admin := range adminGroups {
            if strings.EqualFold(g, admin) {
                return true
            }
        }
    }
    return false
}

// Evaluate returns the MFA method to enforce for the current login.
func Evaluate() (string, error) {
    if isAdmin() {
        return "passkey", nil
    }
    return "disabled", nil // non-admins: defer to static mfa_policy
}

// ReauthInterval returns the per-application MFA re-verification interval.
// Admins are re-challenged every 15 minutes; other users use the global setting.
func ReauthInterval() (int, error) {
    if isAdmin() {
        return 15, nil
    }
    return 0, nil // use global setting
}

Terraform

resource "justiam_application" "my_app" {
  name = "My App"
  type = "oidc"

  mfa_policy             = "optional"
  mfa_selector_script    = file("${path.module}/scripts/mfa_selector.go")
  mfa_selector_fail_open = false
}

More examples

See Script Examples for additional MFA selector scripts:

  • Require passkey for admins — strongest MFA for admin group members
  • Network-based MFA — require strong MFA from untrusted networks
  • Progressive MFA by role — layer MFA by group with custom re-auth intervals
  • Attribute-based MFA — policy driven by user attributes (security level, employment type)