Skip to main content

Preset Framework

Provider presets allow Arcan's setup wizards to auto-fill configuration for common identity providers without requiring a binary upgrade when providers change their URLs, endpoints, or recommended settings.

Architecture

                    ┌──────────────────────┐
│ registry.getarcan.dev│
│ /presets/sso.json │
└──────────┬───────────┘
│ HTTP GET (5s timeout)

┌───────────────┐ ┌──────────────────────┐ ┌───────────────────┐
│ Embedded JSON │◄───│ presets.Load() │───►│ ~/.arcan/presets/ │
│ (in binary) │ │ │ │ sso.json (cache) │
│ Fallback #3 │ │ 1. Check cache (24h) │ │ Priority #1 │
└───────────────┘ │ 2. Fetch registry │ └───────────────────┘
│ 3. Embedded fallback │
└──────────────────────┘

Resolution order:

  1. Local cache (~/.arcan/presets/sso.json) -- if file exists and is less than 24 hours old
  2. Remote registry (registry.getarcan.dev/presets/sso.json) -- fetched with 5-second timeout, then cached
  3. Embedded defaults (internal/presets/defaults.json) -- compiled into the binary, always available

This design ensures the wizard works offline (embedded fallback), stays current when connected (registry fetch), and remains fast on repeat runs (local cache).

JSON Format

The preset file is a single JSON object with three arrays:

{
"version": "2026-04-02",
"oidc": [ ... ],
"saml": [ ... ],
"ldap": [ ... ]
}

OIDC Preset Schema

{
"name": "okta",
"display_name": "Okta",
"issuer": "",
"issuer_template": "https://{{domain}}.okta.com",
"prompts": {
"domain": "Okta domain (e.g., mycompany from mycompany.okta.com)"
},
"console_url": "https://{{domain}}-admin.okta.com/admin/apps",
"docs_url": "https://docs.getarcan.dev/guides/sso/okta",
"default_scopes": ["openid", "email", "profile"]
}
FieldRequiredDescription
nameYesMachine identifier (e.g., okta, google)
display_nameYesHuman-readable name shown in wizard menu
issuerOne ofFixed issuer URL (e.g., Google's https://accounts.google.com)
issuer_templateOne ofTemplate with {{placeholder}} values for dynamic issuers
promptsIf templateMap of placeholder name to prompt label shown to user
console_urlNoURL to the provider's admin console for credential creation
docs_urlNoURL to Arcan's setup guide for this provider
default_scopesNoDefault OAuth2 scopes (omit to use openid, email, profile)

SAML Preset Schema

{
"name": "azure",
"display_name": "Azure AD (Entra ID)",
"metadata_template": "https://login.microsoftonline.com/{{tenant_id}}/federationmetadata/2007-06/federationmetadata.xml",
"prompts": {
"tenant_id": "Azure Tenant ID"
},
"console_url": "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps",
"docs_url": "https://docs.getarcan.dev/guides/sso/azure-saml"
}
FieldRequiredDescription
nameYesMachine identifier
display_nameYesHuman-readable name shown in wizard menu
metadata_templateNoTemplate for metadata URL with {{placeholder}} values
promptsIf templateMap of placeholder name to prompt label. Use metadata_url key for full URL prompts (no template).
console_urlNoURL to the provider's admin console
docs_urlNoURL to Arcan's setup guide
config_hintsNoMap of hint keys to example values

LDAP Preset Schema

{
"name": "active-directory",
"display_name": "Active Directory",
"default_port": 636,
"default_filter": "(&(objectClass=person)(sAMAccountName=%s))",
"default_attrs": {
"user_attr": "sAMAccountName",
"email_attr": "mail",
"name_attr": "displayName",
"group_attr": "memberOf"
},
"config_hints": {
"bind_dn": "cn=arcan,ou=Service Accounts,dc=example,dc=com",
"base_dn": "dc=example,dc=com"
},
"docs_url": "https://docs.getarcan.dev/guides/sso/active-directory"
}
FieldRequiredDescription
nameYesMachine identifier
display_nameYesHuman-readable name shown in wizard menu
default_portYesDefault LDAP port (typically 636 for LDAPS)
default_filterYesDefault user search filter (%s = username)
default_attrsYesMap with keys: user_attr, email_attr, name_attr, group_attr
config_hintsNoMap with keys: bind_dn, base_dn (example values shown in prompts)
docs_urlNoURL to Arcan's setup guide

Template Expansion

Templates use {{placeholder}} syntax (double curly braces). The prompts map defines which placeholders exist and what label to show the user.

Templates are expanded everywhere they appear -- issuer_template, metadata_template, console_url, etc. This means a console URL like https://{{domain}}-admin.okta.com/admin/apps gets the same domain value the user entered for the issuer template.

Adding a New Provider

  1. Edit internal/presets/defaults.json
  2. Add an entry to the appropriate array (oidc, saml, or ldap)
  3. Bump the version field (use date format: YYYY-MM-DD)
  4. Run go build ./cmd/arcan/ to verify the JSON embeds correctly
  5. Submit a PR

The new provider will appear in the wizard menu automatically. No code changes to the wizard functions are needed.

Cache Behavior

ScenarioBehavior
First run, onlineFetches from registry, caches locally, uses result
First run, offlineUses embedded defaults (no cache written)
Repeat run, cache fresh (under 24h)Uses local cache (no network call)
Repeat run, cache stale (over 24h)Fetches from registry, updates cache
Repeat run, cache stale, offlineUses embedded defaults
arcan auth update-presetsForces fetch from registry

The cache file is stored at ~/.arcan/presets/sso.json with 0644 permissions. The presets directory is created with 0700 permissions.

Registry Endpoint

The registry serves presets at:

GET https://registry.getarcan.dev/presets/sso.json

The response must be a valid JSON object matching the preset schema. The version field is used for logging and display purposes only -- cache expiry is based on file modification time, not version comparison.

The fetch timeout is 5 seconds. If the registry is unreachable or returns a non-200 status, the loader silently falls back to the next tier without error messages (debug logging only via slog.Debug).