Merged
Size
L
Change Breakdown
Feature70%
Bug Fix20%
Refactor10%
#27745feat: Add expression-based role mapping strategy

Expression-based SSO role mapping now available

Expression-based SSO role mapping now available

SSO role provisioning now supports expression-based mapping, letting admins write claim evaluation rules instead of relying on fixed claim names.

SSO role provisioning in n8n previously mapped identity provider claims directly to n8n roles — a straightforward but inflexible approach that required exact claim name matches. Expression-based role mapping introduces a more powerful alternative: admins can write evaluation expressions that examine the full claims payload and assign roles conditionally.

When enabled via the new scopesUseExpressionMapping config flag (and the environment variable N8N_ENV_FEAT_ROLE_MAPPING_STRATEGY), the provisioning pipeline evaluates all persisted mapping rules against $claims on every SSO login. Matching rules determine both instance roles and project memberships. Stale project access is revoked when no rule assigns it.

A bug was also fixed: previously, project access revocation was skipped when expression mapping resolved to zero valid assignments. Existing project memberships are now always checked first, ensuring users are removed from projects when rules no longer match them.

The two provisioning strategies are mutually exclusive — the config API rejects attempts to enable both simultaneously. This prevents ambiguous role resolution from the same login flow.

This work completes the IAM-395 feature end-to-end, wiring expression-based mapping into both OIDC and SAML SSO flows.

View Original GitHub Description

Summary

Integrates expression-based role mapping into the SSO provisioning pipeline for both OIDC and SAML, completing the IAM-395 feature end-to-end.

Background: two provisioning strategies

n8n's SSO provisioning pipeline maps identity provider claims to n8n roles on every login. This PR adds a second strategy alongside the existing one:

Direct-claim (existing)

  • Config: scopesProvisionInstanceRole / scopesProvisionProjectRoles
  • Instance role: value of the scopesInstanceRoleClaimName claim
  • Project roles: array value of scopesProjectsRolesClaimName claim (projectId:role strings)
  • Always active when the config enables it

Expression-mapping (new)

  • Config: scopesUseExpressionMapping: true
  • Instance role: first matching type: 'instance' rule evaluated against all claims
  • Project roles: all matching type: 'project' rules, each scoped to specific projects
  • Also requires N8N_ENV_FEAT_ROLE_MAPPING_STRATEGY=true

The two strategies are mutually exclusive — PATCH /provisioning/config rejects the request if scopesUseExpressionMapping is set alongside direct-claim fields.

What this PR does

New config field: scopesUseExpressionMapping (boolean, default false). When true, the expression-mapping path runs instead of the direct-claim path on every SSO login.

OIDC login flow with expression mapping enabled:

  1. User authenticates via OIDC
  2. OidcService.applySsoProvisioning calls isExpressionMappingEnabled()
  3. If enabled: builds a RoleResolverContext from token claims + userInfo via buildOidcClaimsContext, then calls provisionExpressionMappedRolesForUser
  4. ProvisioningService loads all persisted mapping rules, evaluates each expression against $claims, applies matching instance role and project memberships, and revokes any project access not present in the result

SAML login flow with expression mapping enabled:

  1. User authenticates via SAML
  2. SamlService.applySsoProvisioning calls isExpressionMappingEnabled()
  3. If enabled: builds a RoleResolverContext from raw SAML attributes via buildSamlClaimsContext, then calls provisionExpressionMappedRolesForUser
  4. Same provisioning logic as OIDC from this point

Bug fixed: applyExpressionMappedProjectRoles had two early returns that skipped project access revocation when the resolved role map was empty or all mapped projects were invalid. Existing project memberships are now always fetched first and stale access is revoked, even when expression mapping resolves to zero valid assignments.

Manual testing

Prerequisites: an n8n instance with an SSO provider configured (OIDC or SAML), with N8N_ENV_FEAT_ROLE_MAPPING_STRATEGY=true set in the environment, and at least two team projects created.

Scenario 1 — Expression mapping provisions instance role (OIDC or SAML)

  1. Create a mapping rule: type instance, role global:admin, expression {{ $claims.department === 'engineering' }}
  2. Enable expression mapping: PATCH /api/v1/provisioning/config with { "scopesUseExpressionMapping": true }
  3. Log in via SSO as a user whose IdP profile has department = engineering
  4. Expected: user's n8n instance role is global:admin
  5. Log in again with a user whose department differs
  6. Expected: user receives the default member role (no rule matched)

Scenario 2 — Expression mapping provisions project membership (OIDC or SAML)

  1. Create a mapping rule: type project, role project:editor, expression {{ $claims.groups.includes('n8n-editors') }}, scoped to Project A
  2. Enable expression mapping as above
  3. Log in via SSO as a user whose IdP profile has groups = ['n8n-editors', 'devops']
  4. Expected: user is added to Project A as editor
  5. Remove n8n-editors from the user's groups in the IdP and log in again
  6. Expected: user is removed from Project A (stale access revoked)

Scenario 3 — Expression mapping revokes all project access when no rules match

  1. Ensure the user from Scenario 2 is a member of Project A
  2. Delete the mapping rule (or change the expression so it no longer matches)
  3. Log in via SSO
  4. Expected: user is removed from Project A entirely — no stale membership preserved

Scenario 4 — Direct-claim path is unaffected

  1. Disable expression mapping: scopesUseExpressionMapping: false, enable scopesProvisionInstanceRole: true
  2. Log in via SSO with a claim n8n_instance_role: global:admin
  3. Expected: direct-claim provisioning works as before; expression mapping is not invoked

Scenario 5 — Mutual exclusivity enforced

  1. PATCH /api/v1/provisioning/config with both scopesUseExpressionMapping: true and scopesProvisionInstanceRole: true
  2. Expected: 400 Bad Request — "Expression-based mapping and direct-claim provisioning cannot both be enabled at the same time."

Related Linear tickets

https://linear.app/n8n/issue/IAM-395

Review / Merge checklist

  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with Backport to Beta, Backport to Stable, or Backport to v1 (if the PR is an urgent fix that needs to be backported)
© 2026 · via Gitpulse