Skip to main content

B2B via Connections

Use this when each external customer maps to its own enterprise connection (a SAML or OIDC connection to their IdP, for example) instead of an Auth0 Organization. A user's tenant membership follows which connection they authenticated through, and which groups their IdP put them in via SCIM. This is an alternative to B2B via Organizations: no Auth0 Organization is involved anywhere in this model.

A connection is the organization boundary here: a user reaches it either directly, or through a SCIM group that belongs to it.

Model​

model
schema 1.1

type connection # Auth0 connection (e.g. SAML, SCIM enterprise connection); one per customer
relations
define member: [user, group#member] # directly-connected users and group members

type group # SCIM group synced from the connection
relations
define connection: [connection] # which connection owns this group
define member: [user, group#member] # direct members + nested groups

type user

Mapping​

version: "1"

rules:
- name: "Link user to connection"
when: >-
(input.type == "user.created" || input.type == "user.updated") &&
input.data.context?.connection?.id != nil
variables:
user_id: input.data.object.user_id
connection_id: input.data.context.connection.id
tuples:
- user: "user:{{ variables.user_id }}"
relation: "member"
object: "connection:{{ variables.connection_id }}"

- name: "Link group to connection on creation"
when: input.type == "group.created"
variables:
group_id: input.data.object.id
connection_id: input.data.object.connection_id
tuples:
- user: "connection:{{ variables.connection_id }}"
relation: "connection"
object: "group:{{ variables.group_id }}"
- user: "group:{{ variables.group_id }}#member"
relation: "member"
object: "connection:{{ variables.connection_id }}"

- name: "Add user member to group"
when: >-
input.type == "group.member.added" &&
input.data.object.member.member_type == "user"
variables:
group_id: input.data.object?.group?.id
member_id: input.data.object?.member?.id
tuples:
- user: "user:{{ variables.member_id }}"
relation: "member"
object: "group:{{ variables.group_id }}"

- name: "Remove user member from group"
when: >-
input.type == "group.member.deleted" &&
input.data.object.member.member_type == "user"
variables:
group_id: input.data.object?.group?.id
member_id: input.data.object?.member?.id
tuples:
- action: delete
user: "user:{{ variables.member_id }}"
relation: "member"
object: "group:{{ variables.group_id }}"

- name: "Revoke all group tuples on deletion"
when: >-
input.type == "group.deleted" &&
input.data.object?.id != nil
variables:
group_id: input.data.object.id
tuple_filters:
- object: "group:{{ variables.group_id }}"
action: delete
- object: "group:"
user: "group:{{ variables.group_id }}#member"
action: delete
- object: "connection:"
user: "group:{{ variables.group_id }}#member"
action: delete

tests:
- name: "user.created with connection context writes direct connection member"
input:
type: "user.created"
data:
object:
user_id: "u_alice"
context:
connection:
id: "con_acme_saml"
expect_tuples:
- user: "user:u_alice"
relation: "member"
object: "connection:con_acme_saml"

- name: "user.created without connection context writes nothing"
input:
type: "user.created"
data:
object:
user_id: "u_alice"
expect_tuples: []

- name: "group.created links group to connection and grants member access"
input:
type: "group.created"
data:
object:
id: "grp_acme_eng"
connection_id: "con_acme_saml"
expect_tuples:
- user: "connection:con_acme_saml"
relation: "connection"
object: "group:grp_acme_eng"
- user: "group:grp_acme_eng#member"
relation: "member"
object: "connection:con_acme_saml"

- name: "group.member.added adds a user member"
input:
type: "group.member.added"
data:
object:
group:
id: "grp_acme_eng"
member:
member_type: "user"
id: "u_bob"
expect_tuples:
- user: "user:u_bob"
relation: "member"
object: "group:grp_acme_eng"

- name: "group.member.deleted removes a user member"
input:
type: "group.member.deleted"
data:
object:
group:
id: "grp_acme_eng"
member:
member_type: "user"
id: "u_bob"
expect_tuples:
- user: "user:u_bob"
relation: "member"
object: "group:grp_acme_eng"
action: delete

- name: "group.deleted revokes all group tuples"
input:
type: "group.deleted"
data:
object:
id: "grp_acme_eng"
expect_tuples: []
expect_tuple_filters:
- object: "group:grp_acme_eng"
action: delete
- object: "group:"
user: "group:grp_acme_eng#member"
action: delete
- object: "connection:"
user: "group:grp_acme_eng#member"
action: delete

connection#member accepting group#member (alongside user) is what lets a user reach the connection either directly or through a SCIM group, with the same relation either way. "Link group to connection on creation" writes both the bookkeeping group --connection--> connection tuple (which connection owns this group) and the group#member --member--> connection tuple that actually grants every group member connection membership. With that in place, a Check for user#member@connection resolves true for a user added directly and for a user added to any group on that connection, and false for a user who was never added to either.

note

Auth0's event schema for user.created and user.updated only requires context.tenant, so context.connection is optional and can be absent. Don't rely on it to identify the connection. Read data.object.identities[].connection instead, which the schema always includes; it holds the connection's name, not its ID, and is paired with that identity's user_id.

Known limitations​

  • Connection deletion isn't handled above. connection.deleted exists as an event, but this mapping doesn't clean up a user's direct connection membership or a group's connection tuple on deletion; add a tuple_filters rule modeled on "Revoke all group tuples on deletion" if you need this.
  • Roles. There's no per-user role assignment tied to a plain connection. If you need roles here, assign them to the SCIM group itself; see Roles: a group, at the tenant level.

Have Feedback?

You can use any of our support channels for any questions or suggestions you may have.