Self-service

Any signed-in user can create organizations, OIDC clients (apps), audiences (APIs) and app key scopes, without an administrator. This page is for developers and organization admins: who owns what, what each permission allows, and which guards stop people from giving themselves more rights than they have.

Everything here is done in the account dashboard (https://auth.infra.delta.rocks/account/organizations, https://auth.infra.delta.rocks/account/clients, https://auth.infra.delta.rocks/account/audiences). The same operations are available in the manage GraphQL root, which the dashboard calls with your session token; the examples below show them. Calling manage yourself needs a token with the account scope of the server's own audience (the dashboard's) or an admin token, see Admin access.

Ownership

Every client, audience and app key scope has exactly one owner. App key scopes follow the audience rules (org:audiences).

Owner Who can see it Who can change it Notes

You (personal)

you

you

Pick "Personal" as owner when creating. Deleted together with your account.

Organization

every member, read-only

members with org:clients (clients) or org:audiences (audiences)

Pick the organization as owner; you need that permission in it. The organization can't be deleted while it owns clients or audiences.

System

admins only

admins only

Created through the admin API. The dashboard shows them as "system" (verified) or "unowned" (unverified).

In GraphQL the owner is owner { kind id name } with kind one of USER, ORGANIZATION, SYSTEM. Transferring a client or audience to another owner is not available yet.

Organizations

Create one

In the dashboard: Organizations, "New organization". In GraphQL:

mutation {
  manage {
    createOrganization(slug: "acme", name: "Acme Inc.") {
      id
      slug
      viewerPermissions
    }
  }
}

The slug is 1 to 63 lowercase letters, digits and inner dashes. It appears in dashboard URLs and in the organization= authorization parameter, so renaming it breaks old links. You become a member with the role:org-admin role.

Organizations page
Organizations: a pending invitation, your organizations with your permissions, and the create form

An organization's pages are at /account/organizations/<slug>: Overview, Members, Invitations, Roles, Clients, Audiences, Client access and API keys. Some tabs only show up with the matching permission (see [hidden-disabled]).

Overview

Shows your permissions, the organization id, slug and creation date. With org:manage you can edit slug, name, picture and public metadata (a JSON object of at most 8 KiB, visible to the claims hook and to members), and delete the organization. Deleting fails with "delete or transfer organization clients, audiences and app key scopes first" while it still owns any. The organization's API keys are deleted with it. Members can "Leave organization" here.

Organization overview
An organization's Overview tab

Permissions and roles

Organization permissions are scopes of the server's own audience. Members get them through roles, and the server checks them against the database on every management request.

Scope GraphQL Dashboard label Allows

org:manage

MANAGE

Manage organization

Edit slug, name, picture and public metadata; delete the organization.

org:members

MEMBERS

Manage members

Invite (by username, email or link), revoke invitations, change members' roles, remove members.

org:roles

ROLES

Manage roles

Create organization roles, edit their names and grants, delete them.

org:clients

CLIENTS

Manage clients

Create and edit clients owned by the organization.

org:audiences

AUDIENCES

Manage audiences

Create and edit audiences owned by the organization and their scopes; add their scopes to clients.

org:access

ACCESS

Manage client access

Decide which clients can see the organization; approve or deny access requests.

org:api-keys

API_KEYS

Manage API keys

See and revoke organization API keys. Also required from the user named as creator when an app creates one.

Two roles exist in every organization:

  • role:org-admin ("Organization admin"): all seven permissions. The creator gets it.

  • role:org-member ("Organization member"): no permissions. Members can still see the organization, its members, roles, clients and audiences.

Administrators can make further global roles available to organizations; they show up in the role list next to these two, marked "built-in" like them. Organizations can also create their own roles:

mutation {
  manage {
    organization(organization: "acme") {
      createRole(key: "wiki-editors", name: "Wiki editors", description: "Edit the team wiki") {
        id
      }
    }
  }
}

The key follows the slug rules; the role id becomes <organization id>:<key>. A role's grants are organization permissions plus scopes of audiences owned by the same organization:

mutation {
  manage {
    role(roleId: "O...:wiki-editors") {
      setScopes(
        scopes: [{ audienceId: "A...", scopeId: "wiki:write" }]
        permissions: [CLIENTS]
      ) {
        audienceId
        scopeId
      }
    }
  }
}

setScopes replaces both lists; leaving out permissions keeps the current ones. Audience scopes from roles end up in tokens when the user signs in to an app with this organization active, see Organizations and Scopes and claims. In the dashboard: Roles, "New role", then the role's page ("Grants").

Role page with grants
A custom role granting two organization permissions and two scopes of the organization's audience

Members

The Members page lists everyone with their roles and resulting permissions. With org:members, each entry has an "Edit" fold to change roles ("Save roles") and to remove the member. You can't remove yourself there; use "Leave organization" on the Overview.

Members page
Members as seen by an organization admin
mutation {
  manage {
    organization(organization: "acme") {
      setMemberRoles(userId: "U...", roles: ["role:org-member", "O...:wiki-editors"]) {
        id
        name
      }
    }
  }
}

removeMember(userId:) and leave work the same way. All of them are subject to the guards in [guards].

Invitations

Kind Create Who can accept Delivery

Existing user

Members page, "Invite an existing user" (username); invite(user: ...)

only that account

Shows up under Organizations, "Invitations", with Accept and Decline.

Email

Invitations page with an email; invite(email: ...)

any account with that address verified

Emailed by the server when email is configured (see Email). The link is also shown once.

Link

Invitations page with the email left empty; invite(roles: ...) without email and user

anyone who has the link

You share the link, which is shown once.

Every invitation carries roles (the dashboard preselects role:org-member), expires after a number of days (7 by default) and can be revoked while pending. It is used up when someone joins. Links look like https://auth.infra.delta.rocks/invite#<code>; the invitee opens it, signs in and joins.

Invitations page
Pending invitations and the form for a new one
mutation {
  manage {
    organization(organization: "acme") {
      invite(email: "bob@example.com", roles: ["role:org-member"], expiresIn: 604800) {
        url
        invitation {
          id
          expiresAt
        }
      }
    }
  }
}

expiresIn is in seconds. Errors you may see: "invite either by email or by user, not both", "not a valid email address", "user is already a member", "user already has a pending invitation", "no user with that username", "Could not send the invitation email, try again later". If the server has no email configured, no invitation email is sent; pass on the link yourself.

The invitee's side: manage { invitations { id organizationName roles expiresAt } }, then acceptInvitation(id:) or declineInvitation(id:).

Client access

A client only sees an organization (in the sign-in flow, in the orgs claim, in the Backend API) after the organization enabled it, unless an admin made it first-party with "All organizations". Clients owned by the organization are enabled for it when they are created. Everything else goes through the Client access page (org:access):

  • Requests: members who wanted to use the organization with a client that is not enabled. "Approve" enables the client and clears all requests for it; "Deny" drops that member's request.

  • Clients that can see this organization, each with "Revoke access".

  • Allow client: enable a client by its client ID.

Client access page
Client access with a member's request for a third-party app

Members ask for access on the sign-in screen ("Not enabled for this app", "Request access"). Enabling a client does not expose anyone by itself: each member still decides on the sign-in screen whether that app may see their membership.

mutation {
  manage {
    organization(organization: "acme") {
      setClientAccess(clientId: "C...", enabled: true)
    }
  }
}

Denying is denyAccessRequest(clientId:, userId:); without userId it drops all requests for the client.

API keys

With org:api-keys the API keys tab lists the organization's API keys (created by apps through the Backend API) and lets you revoke them (revokeApiKey(id:, reason:)). See Backend API.

Clients (apps)

Create a client

Clients, "New client": choose the owner ("Personal" or an organization where you have org:clients), name, description, type and logo.

  • Confidential: runs on a server and authenticates with a client secret.

  • Public: runs in a browser or as a native app, PKCE only.

Clients page
Your clients and the form for a new one
mutation {
  manage {
    createClient(organization: "acme", name: "Wiki", description: "Team wiki", clientKind: CONFIDENTIAL) {
      clientId
      allowedGrantTypes
    }
  }
}

Leave out organization for a personal client. A new client allows the authorization code and refresh token grants, requires PKCE, requires a client secret if it is confidential, uses public global subject identifiers, does not use organizations and is unverified.

Client pages

Tab What you set

Settings

Name, description, logo, type, "Require client secret", "Require PKCE", ID token signature (RS256 or ES256), Organizations ("Not used", "Optional", "Required", see Organizations). The "Backend API" section turns the client secret into a server key, see Backend API. "Delete client" at the bottom.

Client secret

Confidential clients only. "Issue secret" or "Rotate secret". The secret (DELTA-CLIENT-SECRET-...) is shown once; rotating invalidates the old one immediately. A confidential client has no secret until you issue one, and token requests fail until then.

Redirect URLs

Allowed redirect URLs, one per line, http or https, exact match. The optional "Default URL" is used when a request has no redirect_uri; anyone who knows the client ID can discover it. Below: allowed post-logout redirect URLs.

Grant types

Authorization code, Refresh token, Device code, Backchannel (CIBA). Implicit is not supported, and Client credentials is not available yet. CIBA needs a client secret, see Backchannel authentication.

Scopes

Allowed scopes the client may request, and required scopes it always gets. The picker lists audiences you can manage. Built-in scopes such as openid and profile are always available.

App keys

App key scopes the client may request as app_key:<id>. The list shows scopes you manage plus the ones already allowed. See [_app_key_scopes].

Subject IDs

What sub looks like: public (same for every client) or pairwise (per sector), global or per organization, and which sibling clients share a pairwise sector. See Scopes and claims.

Organizations

Read-only list of organizations that enabled this client.

Client settings
The Settings tab of an organization's client
Client secret tab
A new secret right after "Rotate secret": masked, with buttons to show and copy it
Redirect URLs tab
Redirect URLs with a default URL, and post-logout redirect URLs
Scopes tab
Allowed and required scopes from the organization's audience

Configuring by GraphQL (each set... replaces the whole list; resetClientSecret returns the new secret):

mutation {
  manage {
    client(clientId: "C...") {
      setRedirectUrls(urls: [{ url: "https://wiki.example.com/oauth/callback", public: true }]) {
        redirectUrl
      }
      setAllowedGrantTypes(grantTypes: [AUTHORIZATION_CODE, REFRESH_TOKEN])
      setAllowedScopes(scopes: [{ audienceId: "A...", scopeId: "wiki:read" }]) {
        scopeId
      }
      resetClientSecret
    }
  }
}

Other mutations on a client: update(name:, description:, picture:, clientKind:, usesClientSecret:, usesPkce:, idTokenSignatureKind:, organizationUsage:), setPostLogoutRedirectUrls(urls:), setRequiredScopes(scopes:), setAllowedAppKeyScopes(scopeIds:), setSubject(subjectType:, subjectScope:, shareWith:, force:), setBackendApi(enabled:), delete.

Changing subject settings of a client that already issued tokens or has consents needs force: true ("Change even if users already signed in"); every existing user then gets a new sub.

Note
Allowing a scope on a client does not give it to users. A user only gets an audience scope through a role: for organization audiences, an organization role assigned to them (active while that organization is selected at sign-in); otherwise a global role, which only admins can set up.

Audiences (APIs)

An audience is an API that clients request tokens for; its scopes are what users consent to.

Audiences, "New audience": owner, name, description, identifier and picture. The identifier must be unique, usually the API's URL (for example https://api.example.com); clients pass it as the audience parameter. On the audience's Scopes tab, "Add or update scope" takes a scope id (for example read:items), name and description; "Remove" deletes one.

Audience scopes
An audience's Scopes tab
mutation {
  manage {
    createAudience(
      organization: "acme"
      name: "Wiki API"
      description: "Wiki REST API"
      audienceIdentifier: "https://wiki.example.com/api"
    ) {
      audienceId
    }
  }
}
mutation {
  manage {
    audience(audienceId: "A...") {
      declareScope(scopeId: "wiki:read", name: "Read the wiki", description: "Read pages") {
        scopeId
      }
    }
  }
}
  • Access tokens are signed with RS256 (default) or ES256 (signatureKind on create, accessTokenSignatureKind on update); other algorithms are rejected.

  • Scope ids: up to 128 printable ASCII characters, no spaces, double quotes or backslashes, not a built-in scope name and not starting with app_key:.

  • update(...) changes name, description, picture, identifier and signature; delete removes the audience with its scopes (not possible for system audiences).

App key scopes

An app key scope names an end-to-end encryption key that clients can ask users for, see App encryption keys.

App key scopes, "New app key scope": owner, scope id, name, description and "Client-bound". The scope id (clients request it as app_key:<id>) and "Client-bound" can't be changed later. Each entry shows how many clients may request the scope and how many users have a key for it; "Edit" changes name and description, "Delete app key scope" deletes it together with every user's key.

mutation {
  manage {
    createAppKeyScope(scopeId: "luks", name: "Disk unlock", clientBound: true) {
      scopeId
    }
  }
}
  • Scope ids: up to 128 printable ASCII characters, no spaces, double quotes or backslashes, not starting with app_key:, unique across the server.

  • appKeyScope(scopeId:) { update(name:, description:) delete }; delete is not possible for system scopes.

The consent screen and the organization screen show who published the app:

Client Publisher line

system-owned and verified ("first-party")

none

system-owned, not verified

"by Unknown publisher" with an "Unverified app" badge

owned by a user

"by <name>", with "Unverified app" until verified

owned by an organization

"by <organization name>", with "Unverified app" until verified

A user's name is their profile name, else their username. Only administrators can verify a client, see Mark an app verified. For personal and organization clients verification only removes the badge; a verified system client becomes first-party. In the dashboard, client lists show a "verified" or "unverified" badge and an owner badge.

Permission rules and escalation guards

All checks run on the server. An admin token (admin mode or GraphiQL) skips them; an admin using a normal session is treated like everyone else.

Visibility
  • Personal clients and audiences: only the owner.

  • Organization clients, audiences and roles: every member of the organization.

  • System clients and audiences: only admins.

  • Organizations: only members.

Editing
  • A client or audience needs ownership, or org:clients / org:audiences in the owning organization.

  • An organization role needs org:roles in that organization. Global roles, including role:org-admin and role:org-member, can only be edited by admins, and only organization roles can be deleted.

  • First-party clients and system audiences can't be deleted through manage.

Assigning roles (invitations and member roles)
  • The role must belong to this organization or be a global role that admins made organization-assignable, otherwise "role can't be assigned in this organization".

  • The role's permissions must all be permissions you hold yourself, otherwise "permission denied".

Changing or removing a member
  • The member's current permissions must be a subset of yours: you can't demote or remove someone with more rights.

Role grants
  • You can only add organization permissions you hold; permissions a role already has can be kept.

  • Audience scopes must come from audiences owned by the same organization ("organization roles may only include scopes of organization audiences"). Other scopes of the server's own audience are rejected.

Keeping a member manager
  • Any change that would leave no member with org:members fails with "change would leave no member able to manage members": changing a member's roles, removing a member, leaving, changing a role's grants or deleting a role. The last remaining member may still leave.

Client scopes
  • Adding allowed or required scopes of an audience needs the right to manage that audience (owner, or org:audiences in its organization), otherwise "you can't grant scopes of audience <name>". Removing scopes is always allowed.

  • Adding an app key scope to a client needs the right to manage that scope, otherwise "you can't grant app key scope <name>". Removing is always allowed.

Admin only
  • setVerified, setAllOrganizations, setOrganizationAssignable and organizations(all: true).

Redirect URLs
  • Clients created through dynamic registration with a verified pairwise sector must keep their redirect URLs inside that sector.

What is hidden and what is disabled

The API tells the dashboard what you may do, and the dashboard hides actions you can't take and disables fields you can't change.

Members page seen by a regular member
The same Members page for a member without org:members: no Invitations, Client access or API keys tabs and no edit controls
query {
  manage {
    viewer {
      userId
      admin
    }
    organization(organization: "acme") {
      viewerIsMember
      viewerPermissions
      invitations { id }
      clients {
        clientId
        viewerCanEdit
        owner { kind name }
      }
    }
  }
}
  • viewer.admin is true only for admin tokens.

  • ManagedOrganization.viewerPermissions lists your permissions in the organization (all of them for admins); viewerIsMember is false when an admin looks at an organization they don't belong to.

  • ManagedClient, ManagedAudience and ManagedRole have viewerCanEdit.

  • Permission-gated organization fields return null instead of an error: invitations (needs MEMBERS), accessRequests and enabledClients (ACCESS), apiKeys (API_KEYS). The matching dashboard pages answer 404.

  • Mutations you are not allowed to run fail with "permission denied".

Dashboard element Without the right

Invitations, Client access, API keys tabs

hidden without org:members, org:access, org:api-keys

Organization edit form and "Delete organization"

hidden without org:manage; "Leave organization" only for members

Member "Edit" fold, "Invite an existing user"

hidden without org:members; no remove button on yourself

"New role"

hidden without org:roles

Role page fields

disabled unless viewerCanEdit; permission checkboxes you don't hold are disabled and keep their state

"New client" / "New audience" links on organization pages

hidden without org:clients / org:audiences

Owner select on "New client" / "New audience"

lists only organizations where you have org:clients / org:audiences

Client and audience pages

fields disabled and a "read-only" badge unless viewerCanEdit; save buttons, "Backend API", scope add and remove, secret rotation and "Delete" hidden

"Mark as verified", "All organizations (first-party)"

admin mode only