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 |
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.
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.
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 |
|---|---|---|---|
|
|
Manage organization |
Edit slug, name, picture and public metadata; delete the organization. |
|
|
Manage members |
Invite (by username, email or link), revoke invitations, change members' roles, remove members. |
|
|
Manage roles |
Create organization roles, edit their names and grants, delete them. |
|
|
Manage clients |
Create and edit clients owned by the organization. |
|
|
Manage audiences |
Create and edit audiences owned by the organization and their scopes; add their scopes to clients. |
|
|
Manage client access |
Decide which clients can see the organization; approve or deny access requests. |
|
|
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").
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.
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); |
only that account |
Shows up under Organizations, "Invitations", with Accept and Decline. |
Invitations page with an 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; |
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.
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.
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.
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 ( |
Redirect URLs |
Allowed redirect URLs, one per line, |
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 |
App keys |
App key scopes the client may request as |
Subject IDs |
What |
Organizations |
Read-only list of organizations that enabled this client. |
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.
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.
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 (
signatureKindon create,accessTokenSignatureKindonupdate); 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;deleteremoves 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 };deleteis not possible for system scopes.
Verified apps and the consent screen
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:audiencesin the owning organization. -
An organization role needs
org:rolesin that organization. Global roles, includingrole:org-adminandrole: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:membersfails 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:audiencesin 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,setOrganizationAssignableandorganizations(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.
org:members: no Invitations, Client access or API keys tabs and no edit controlsquery {
manage {
viewer {
userId
admin
}
organization(organization: "acme") {
viewerIsMember
viewerPermissions
invitations { id }
clients {
clientId
viewerCanEdit
owner { kind name }
}
}
}
}
-
viewer.adminis true only for admin tokens. -
ManagedOrganization.viewerPermissionslists your permissions in the organization (all of them for admins);viewerIsMemberis false when an admin looks at an organization they don't belong to. -
ManagedClient,ManagedAudienceandManagedRolehaveviewerCanEdit. -
Permission-gated organization fields return
nullinstead of an error:invitations(needsMEMBERS),accessRequestsandenabledClients(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 |
Organization edit form and "Delete organization" |
hidden without |
Member "Edit" fold, "Invite an existing user" |
hidden without |
"New role" |
hidden without |
Role page fields |
disabled unless |
"New client" / "New audience" links on organization pages |
hidden without |
Owner select on "New client" / "New audience" |
lists only organizations where you have |
Client and audience pages |
fields disabled and a "read-only" badge unless |
"Mark as verified", "All organizations (first-party)" |
admin mode only |