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:
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:
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()returnsmap[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[]stringand JSON-decoded[]interface{}values. Use this for multi-valued claims likegroupsorroles.
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)