Grouper — What it is and how to use it
Grouper is an internal web application for managing Microsoft Entra ID (Azure AD) security groups, app registrations and machine credentials for a small set of first-party applications. It lets the people who build an application administer that application's sign-in and permissions themselves, without an Azure Portal role and without a ticket for every change.
Grouper is reachable on the internal network only. From outside the VPN the host does not resolve, and that is the outermost protection layer of everything described below.
Who this page is for#
This page has three audiences, and the sections are labelled so you can skip the ones that are not yours.
| If you are… | Read | Skip |
|---|---|---|
| An application manager — you own an app in Grouper and decide who may do what in it | What Grouper solves, Core concepts, Everyday workflows, FAQ and troubleshooting | The machine API |
| A developer wiring sign-in into an application | Core concepts, Wiring SSO into your application, The machine API, Operations and security | Most of Everyday workflows |
| A Tool-Admin — you hold an Entra application-administrator role | All of it, especially Why Grouper instead of rolling your own auth and Operations and security | — |
Nothing on this page is a permission. Every statement about who may do what is enforced on the server; the interface merely reflects it.
What Grouper solves#
The problem. A first-party application needs three things that all live in Entra ID: an app registration (so people can sign in), security groups (so the application can tell a team lead from a read-only user), and a client secret (so the application can authenticate itself). Creating and maintaining those in the Azure Portal requires directory roles that developers do not have and should not have. The result is a ticket for every group, every member, every rotated secret — and a directory that drifts, because nobody who knows the application can touch it.
What Grouper is. A narrow, purpose-built surface over exactly those objects:
- It creates and maintains app registrations for managed applications.
- It creates and maintains the SSO rights groups belonging to each application, and their membership.
- It rotates client secrets and shows the value once.
- It inventories machine credentials (API keys, provider secrets) and watches their expiry.
- It serves a small machine API so an application can fetch its own configuration and verify its own tokens at runtime.
What Grouper deliberately is not.
- Not a user directory. It stores no users, no passwords, no password hashes, no sessions of its own beyond the transient sign-in state.
- Not a database. Grouper is stateless: Entra ID is the single source of
truth. What state exists beyond Azure lives in group-name prefixes, in Open
Extensions on the group objects, in the signed-in user's own Entra role
assignments, in a 1Password vault for credentials, and in one server-side
settings.json. - Not an authorization engine. It does not decide what a right means. It carries names and group memberships; your application interprets them.
- Not a general Azure administration console. It can only touch the objects it owns. See Why Grouper instead of rolling your own auth.
Core concepts#
Azure is the source of truth#
There is no Grouper database to get out of sync. Every list you see is read from Microsoft Graph at request time, and every change you make is a write to Graph. Two consequences you will feel:
- Deleting a group in Grouper deletes it in Entra, permanently.
- Graph is eventually consistent. A write is not always visible to the very next read. Grouper therefore updates the list in front of you immediately and reconciles against Graph in the background — see Operations and security.
The group-name grammar#
Every group Grouper manages lives in one namespace, and the separator is a pipe:
| Kind | Name | Meaning |
|---|---|---|
| Manager group | Grouper|<AppKey>|Manager | Membership in it makes you a manager of that application. |
| Rights group | Grouper|<AppKey>|<name> | An SSO right of that application. Its object id lands in your users' tokens. |
The AppKey is the short key of an application — ECTEAM, TOKADA CONCEPTS.
It may be almost any Azure-legal name: spaces, hyphens and mixed case are all
fine, up to 100 characters, with no leading or trailing whitespace. The single
forbidden character is the pipe itself.
Why the pipe matters, and why it is not cosmetic. The server-side
authorization boundary is a prefix comparison: you may write to a group whose
name starts with Grouper|<AppKey>| for an AppKey you manage. Under the older
hyphen-separated scheme Grouper-TOKADA- was a legitimate prefix of
Grouper-TOKADA-CONCEPTS-Users, so a manager of TOKADA could reach the groups
of TOKADA CONCEPTS — a real privilege leak. With the pipe reserved, the two
namespaces are provably disjoint. That is the whole reason the character is
banned inside an AppKey.
Manager is a reserved rights-group name (case-insensitive). A rights group
literally called Manager would collide with the manager group's name and hand
out manager privileges. The server rejects it at create and at rename time.
Rights-group names follow the same shape as AppKeys, with one addition: # is
forbidden too, because #MailEnabled is the suffix that marks a mail-enabled
mirror group (see Mail-enabled mirror groups).
Two roles, and only two#
| Role | How you get it | What it lets you do |
|---|---|---|
| App manager | Membership in Grouper|<AppKey>|Manager | Everything about your applications: rights groups, members, redirect URIs, client-secret rotation, the rights catalogue, API keys of that app. |
| Tool-Admin | An active Entra directory role: Application Administrator or Cloud Application Administrator | Everything an app manager can do, for every app, plus: creating applications, renaming them, deciding who manages them, issuing privileged keys, and the global settings. |
Two details that cause real confusion:
- A PIM-eligible role is not an active role. If your admin role is eligible but not activated, Grouper sees a non-admin.
- Visibility equals membership. Grouper shows you exactly the applications whose manager group you belong to. If you see nothing, you are in no manager group — that is not a bug.
What only a Tool-Admin can do for you#
This list is short on purpose, and everything on it is a deliberate boundary rather than an oversight:
- Create the application. Creating it also creates its Entra app registration, its service principal, and its manager group.
- Add you to
Grouper|<AppKey>|Manager. Who manages an application is not self-service — otherwise a manager could grant themselves peers. - Rename the application (its Entra display name).
- Issue a client key for the machine API. The
clientrole is privileged: a key carrying it can read every published token hash of its app, which makes it a master key over that app's tokens. It is deliberately not offered to delegated app managers in the interface, and the server enforces the same rule. - Switch Entra delivery on and tick the
fetchableopt-in on an individual vault item.
If your application is not listed under Applications, none of the rest of this page applies to you yet. Ask a Tool-Admin. It is not a defect in your code.
Two kinds of application — and neither is a type#
An application in Grouper has two independent capabilities, and it can acquire either one at any point in its life:
| Capability | The app has it when… | It unlocks |
|---|---|---|
| Entra | an Entra app registration is recorded for it | SSO sign-in, redirect URIs, a client secret |
| API | it owns a client-role key | the machine API, consumer-key management, the rights catalogue |
Neither is stored as a flag. Each is derived from whether the underlying object exists, so the two can never disagree with reality. An application created with no app registration ("keys-only") can be given one later; an Entra-backed application can be given its own machine API later.
One known limitation of a keys-only application: its display name cannot be changed after creation, because the rename path goes through the app registration. Retrofitting an app registration also restores the rename path.
Navigation#
| Entry | Who sees it | What is there |
|---|---|---|
| Applications | everyone signed in | Your applications, searchable by name or client ID. A Tool-Admin sees all of them and can filter by kind and creator and sort by name. The chevron beside the entry opens a switcher for jumping straight to any application you can see. |
| API Keys | Tool-Admins | The global 1Password inventory across all apps, including orphaned items whose owning app is unknown. |
| Users | Tool-Admins | The user directory: per user, which applications and groups they are in, and where they are a manager. |
| Shared Items | Tool-Admins | Root-managed credentials usable across applications — created, revealed, rotated and deleted only here. A red counter on the entry shows how many access requests are waiting. |
| Admin | Tool-Admins | Grouper's own application (SYSTEM) — where the tool's own machine keys live. |
| Settings | Tool-Admins | Azure identity, 1Password vault, Jira connection, expiry lead time, scheduled tasks. |
Inside one application the tabs are Application · Groups · Rights · API Keys ·
n8n · Manager, plus Runs and Jira health on SYSTEM only. Tabs that
depend on a capability the app does not have are hidden; a hidden tab is never a
security boundary, because every page re-checks authorization on the server.
Everyday workflows#
All of these assume you are on the VPN, signed in with your Entra account, and a member of the relevant manager group.
Getting an application created#
Send a Tool-Admin three things: the display name, the AppKey you want, and who should manage it. Creating the application produces, in one operation:
- the Entra app registration — single tenant,
groupMembershipClaimsset toSecurityGroup, base permissionUser.Read, and deliberately no secret; - the service principal (the enterprise application);
- the manager group
Grouper|<AppKey>|Manager, with the named managers in it; - Grouper itself as an owner of the app registration — this is what allows secret rotation to work for you later without an Entra role of your own.
Creation is all-or-nothing: if the metadata write fails after the manager group was created, the group is removed again, so a half-built application never occupies its AppKey.
After being added to a manager group, sign out and back in. Group memberships only appear in a newly issued token.
Maintaining redirect URIs#
Applications → your app → Entra Settings. The card loads the redirect URIs currently registered in Entra; add or remove rows and click Save changes. There are deliberately no templates: every developer enters their own URIs.
- The URI must match the origin of your running application exactly —
scheme, host, port, path, trailing slash. A mismatch here is the single most
common sign-in failure (
AADSTS50011). - For
localhost, Entra permitshttp. - The display-name field appears only for Tool-Admins; renaming is on their list above.
Rotating the client secret#
Applications → your app → generate / rotate secret. There is exactly one operation, and it both creates and rotates.
- Lifetime: minimum 14 days, maximum 24 months (an Azure hard limit), default 12 months.
- The value is displayed exactly once, with copy-to-clipboard. Grouper never stores it, never logs it, never mails it, and cannot recover it.
- While that dialog holds the only copy, it will not let you leave: there is no
close cross,
Escapedoes nothing, and a click beside it does nothing. Use the button at the end — that is deliberate, because everything else on this page would discard a value nobody can get back. The dialog shakes when it refuses. - The exception is an application that serves its Entra config over the API: Grouper does hold a copy of the secret, the dialog says so, and it closes normally. If the same step also creates a client key, that key has no copy anywhere, so the dialog holds you until you confirm.
- Put it into your application's own configuration — never into Git.
- Lost it? Do not search for it. Rotate again. The previous secret keeps working until you invalidate it from the list, so switch over first, then remove it.
The application list shows the secret's expiry date and turns it red once it enters the lead-time window (configurable in Settings, default 7 days) or has expired. A daily scheduled scan also files a Jira task ahead of expiry, so you do not have to watch the column.
Creating rights groups#
Applications → your app → Groups.
You type only the <name> part; Grouper supplies the prefix. The result is
Grouper|<AppKey>|<name> — for example Grouper|ECTEAM|Teamlead. The greyed-out
prefix in the dialog is display only; the name stored in Entra contains it in
full.
Renaming and deleting happen here too. Deleting removes the group from Entra permanently, and every user who held a right through it loses it at their next token refresh.
Managing members — users and groups#
Open a group and use the People / Groups switch in its Members tab:
- People searches all users in the tenant.
- Groups finds security groups in the tenant, mail-enabled ones
included; distribution lists, Microsoft 365 groups and Grouper's own
Grouper|…groups never appear and cannot be nested.
Nesting a security group inside a rights group gives every member of that group
the right — putting All-Sales-EMEA into Grouper|ECTEAM|Teamlead instead of
maintaining forty individual users. The icon at the start of each row tells you
which kind of member it is.
Nesting applies to rights groups only. The manager group takes individual users, added by a Tool-Admin. Otherwise a nested group could quietly turn whole departments into application managers.
Maintaining the rights catalogue#
Applications → your app → Rights. A right is a name your application defines and interprets; Grouper only carries it. The catalogue belongs to the application's developers, not to the Tool-Admins, so managers maintain it themselves.
- Format: lowercase
area.action, e.g.markitdown.convert. The dot is mandatory, so a bare word is never a right. - Maximum 64 characters per name, maximum 64 entries per application. The limit is not taste: the catalogue lives in a single Open Extension that holds roughly 2 KB in practice, and Graph does not fail gracefully beyond that.
- Reading is tolerant, writing is strict. A malformed name is refused when you save it; an existing right that no longer parses is quietly ignored on read, so that editing the catalogue can never invalidate tokens that are already in flight.
Rights carried on a key are checked against this catalogue when the key is written, never when it is read. That asymmetry is deliberate: a rename must not retroactively devalue a live token in the middle of a request.
API keys and the rights on a key#
Applications → your app → API Keys lists the credentials inventoried for your application in 1Password. Grouper does not issue third-party keys — it is an inventory plus an expiry watchdog, and normally you paste the value the provider gave you. But for providers where you choose the secret (an MS-SQL login, for example), the create and rotate dialogs offer a Generate button: Grouper mints a strong 32-character password (letters, digits and symbols) straight into the vault. You still have to set that value at the provider yourself — until you do, only the vault knows it — and Reveal is how you collect it. A Tool-Admin additionally sees the global inventory under API Keys, including orphans whose owning application cannot be determined.
Each vault item can carry any combination of three roles, which are independent because they describe independent facts:
| Role | What it grants | Who may set it |
|---|---|---|
client | May call the machine API at all. Its appKey becomes the scope of every answer. | Tool-Admin only |
published | Its hash is delivered in the token list, and it is exactly the set the validate endpoint answers from. | App manager |
fetchable | Its plaintext — together with the item's stored username and all of its custom fields — may be fetched through the secrets endpoint. Store no unrelated secrets in a fetchable item's custom fields. | Tool-Admin only, per item |
An item with no role is invisible to the API entirely. That is the safe default, and it is why the same application can have Grouper store a provider secret without it ever appearing in any list.
To restrict a fetchable secret to specific consumer keys, create a right in the application's Rights tab and tick it on both the secret item and the intended consumer key: a fetchable item with rights ticked is then delivered only to a client key sharing at least one of them, while an item without rights stays fetchable by every client key of the application.
A fourth role, entra, exists and is machine-managed: it marks the one item
holding an application's own Azure client secret. Nobody can set it by hand,
because it is not a permission to grant but a claim about where a value came
from.
The 32-character minimum length applies to publishing only (an unsalted hash of a short value would be reversible): a shorter stored secret is excluded from the token list but stays fetchable through the secrets endpoint like any other opted-in item.
Every item needs either an expiry date or a "check again by" date, so the expiry scan has something to watch. Revealing a value writes an audit line that records who revealed what and when — never the value.
One freshness rule is worth knowing: every change made through Grouper shows up immediately, but an edit made directly in 1Password, past Grouper, can take up to 15 minutes to appear — the inventory is served from a short-lived cache that protects the shared vault quota, and only Grouper's own writes can invalidate it instantly.
Which vault all of this lives in is chosen under Settings on the 1Password
tab: a Tool-Admin picks one of the service account's vaults from a list, and
Grouper stores that vault's ID (the name is shown for recognition only). While
no vault is selected, Grouper falls back to resolving a vault by name,
Grouper by default. Switching vaults drops every 1Password cache, so the next
listing reads the newly selected vault live.
Shared items — one credential, many applications#
Some provider credentials are not owned by one application: a read-only
database login, a company-wide SaaS token. Storing a copy per app means every
rotation is a hunt. A shared item stores it once, at the root level, under
the reserved scope SHARED.
How the pieces fit:
- Tool-Admins manage shared items on the Shared Items page — create, edit, reveal, rotate, delete, and decide which applications may use each item. Application managers cannot; the item does not belong to any app. Create and edit carry the full metadata set of a provider key — client ID, environments, rotation URL and custom fields; only API roles and rights are fixed to the shared shape. Every row offers its actions inline, including a direct link to the provider's rotation page once a rotation URL is set.
- Access is approved per application, by a Tool-Admin. An app's developers can request access to a shared item from the Shared items section of their app's key page; the request waits as pending until an admin approves or rejects it on the Shared Items page, and an admin can also grant or revoke access directly there. Open requests are counted on the Shared Items navigation entry. A pending request grants nothing. Once an application is approved, every API client key of that app can fetch the item.
- The consumer fetches it like any other secret, via
GET /api/v1/secrets/{name}with the shared item's API name. The response'sappKeyis the caller's own app, notSHARED— from the consumer's point of view nothing about the fetch changes. An app without approval — pending or not — gets the same 404 as for a name that does not exist. If the app also owns a local secret under the same name, the local one wins. - An existing key can be converted. A Tool-Admin can turn an app's fetchable provider secret into a shared item from the app's key list; the app it came from is approved automatically, so its consumers keep fetching without interruption. Afterwards the item is managed on the Shared Items page only.
- Deleting a shared item is not blocked by approvals — the delete dialog lists the approved applications first, and after deletion those consumers get a 404.
Mail-enabled mirror groups#
Microsoft's ApplicationAccessPolicy accepts only mail-enabled security
groups as a scope — an object class Graph can read but neither create nor
populate. Grouper closes that gap by keeping a one-way mirror of a rights group
in Exchange Online.
Open a group's Mail mirror tab and switch the mirror on. Two things to understand before you do:
- The opt-in is immediate; the Exchange object is not. The nightly sync builds it. Until then the status line says so, and the row indicator on the groups list can lag the opt-in by up to a night. That is not a display bug — before the run there genuinely is no mirror.
- The mirror is machine-owned. Its membership is overwritten on every sync, so editing it by hand in the Exchange admin centre is silently reverted at the next run. Every mirror carries a description saying exactly that.
The row indicator answers existence, never health: a failed sync is not visible on the row.
Managing n8n credentials#
An application can be connected to an n8n (opens in a new tab) instance, so that the instance's workflow credentials are created, rotated and deleted through Grouper instead of by hand in the n8n UI. The authoritative value always lives in the 1Password vault; n8n is purely a deploy target — its API never returns credential values, so there is no way back and no sync conflict, only the question of whether the vault value has been deployed.
Connecting is a Tool-Admin step, done on the Application tab: base URL
plus an instance API key, entered once and never shown again. Grouper verifies
the connection first — an instance whose public API cannot update credentials is
refused (GRP-N8N-1005) and nothing is stored. After connecting, the n8n
tab appears; disconnecting (also Tool-Admin, on that tab) removes the stored key
and base URL but deletes nothing in n8n, and managed credentials keep their
vault linkage for a later reconnect.
The n8n tab compares the vault against the instance live and sorts every credential into one of three states:
- Managed — vault item and instance credential are linked. App managers can create new credentials (name, type, values, expiry), rotate values (deployed to n8n first, recorded in the vault only after the deploy succeeds; the credential id stays stable, so workflows are untouched — and like a client secret, the old value stops working immediately), reveal the stored values (audited, like every reveal), and delete — which removes the credential in n8n and archives the vault item. Deleting breaks every workflow that references the credential on its next run; the dialog says so before you confirm.
- Unmanaged — the credential exists only in n8n. Adopt it by re-typing its values: they are deployed to the instance first, which is the proof they are right, then stored in the vault. Or mark it ignored to record that it stays unmanaged on purpose.
- Undeployed — the vault item points at a credential the instance no longer has. V1 offers no re-provisioning; the row is the record of what disappeared.
The list is sorted by name (the Name header toggles the direction) and can be filtered: by state, by credential type, by free text, by workflow tag, and by whether a referencing workflow is active or inactive. The free-text field matches the credential's name and type plus the name and folder name of every workflow referencing it — typing a workflow name surfaces the credentials it uses (until usage has loaded, it matches the credential fields only). The tag and active-state filters keep a credential when at least one workflow referencing it matches, and they unlock once usage has loaded. A Used by column loads in the background and counts the workflows referencing each credential — the workflows themselves appear as indented rows directly beneath (active ones first, alphabetical within each group), each prefixed with its folder name when it sits in one and linking into the n8n editor. Active workflows are green, inactive ones red and marked. Each workflow row also shows its tags and the nodes the credential sits in: enabled nodes green, disabled ones red, and a node with a known id links straight to that node inside the n8n editor. Archived workflows are excluded, so an archived copy never counts as usage. Workflows that reference a credential by name instead of id (older exports do) are counted too, as long as exactly one credential matches that name and type. Usage is auxiliary data: if it cannot be loaded — or the instance's workflow listing does not expose node data at all — the column says "unknown" and everything else keeps working, and on very large instances only the first 1,000 workflows are counted. When usage is known, the delete dialog names the affected workflows instead of the generic warning.
Instance data is cached for 30 minutes: the header shows the time the data was fetched ("Data as of"), and its refresh button pulls a fresh snapshot from n8n on demand. Every change made through Grouper refreshes the cache by itself — the button is for changes made directly in n8n.
The credential types managers may create are governed by a Tool-Admin
allowlist under Settings → n8n. While that list is empty — the shipped
state — the four standard HTTP auth types (httpHeaderAuth, httpBasicAuth,
httpBearerAuth, httpCustomAuth) are active, so credentials can be created
without any Settings visit. A maintained list replaces the defaults entirely:
an admin can restrict below them or extend beyond them. OAuth types are
excluded by design: their tokens live in n8n's own consent flow, so Grouper
could never hold the authoritative value.
One governance rule makes the whole model work: credential values change only through Grouper. A value edited directly in the n8n UI drifts silently — n8n never reports values back, so no tool can detect the difference.
Looking up a user in the directory#
The Users page (Tool-Admins only) answers the reverse question the per-app Groups tabs cannot: this person — which applications are they in, through which groups, and where are they a manager? Only users with at least one Grouper membership appear; there is no full tenant listing.
Each row expands to show, per application, the rights groups behind the membership. A group annotated via <group> is a transitive membership: the user is a member of that nested security group, which is itself a member of the rights group. To sever the path, remove the named nested group from the rights group — the user is not a direct member you could remove. Where the user is a manager, the app badge carries a shield and the expanded row a Manager badge.
The directory is a snapshot cached for up to 15 minutes — the header shows when it was built ("Data as of") and its refresh button forces a fresh one. Changes made through Grouper (members, managers, groups, applications) refresh it by themselves. Mail-enabled mirror groups are excluded: their members are a copy of the source group's, so listing them would only duplicate every line.
The directory is honest about holes: when some groups could not be read, a banner names them and warns that memberships may be incomplete. When a fresh build fails entirely, the page keeps showing the last loaded state under its original timestamp rather than pretending to be current.
Wiring SSO into your application#
This section is for the developer integrating sign-in. It assumes the
application was created through Grouper, so its registration already carries
groupMembershipClaims: "SecurityGroup".
What actually arrives in the token#
The ID and access tokens of a signed-in user carry a groups claim, and it
is a list of object ids (GUIDs) — not names. Cloud-only groups do not deliver
names in the claim at all.
{
"aud": "<your client id>",
"oid": "…",
"groups": ["6f2a1e4c-…", "b91c77aa-…"]
}Mapping a GUID to a right#
Every group row in Grouper shows its object id with a copy button. The simplest integration is a static map in your application's configuration:
// config: GUID (copied from Grouper) -> right in your application
const ROLE_BY_GROUP_ID = {
'6f2a1e4c-....': 'teamlead',
'b91c77aa-....': 'readonly',
} as const;
export function rolesFromToken(claims: { groups?: string[] }): string[] {
return (claims.groups ?? [])
.map((id) => ROLE_BY_GROUP_ID[id as keyof typeof ROLE_BY_GROUP_ID])
.filter(Boolean);
}If you have many groups and do not want a deploy per group, load the groups once
through Graph instead and cache a GUID-to-name map: filter on
startswith(displayName,'Grouper|<AppKey>|'), sent with ConsistencyLevel: eventual and $count=true.
Five rules you must not skip#
- Always filter to your own groups. The
groupsclaim contains every security group the user belongs to, not only yours. Check against your known GUIDs, or against theGrouper|<AppKey>|prefix. "The user is in some group, therefore let them in" is a vulnerability, not a shortcut. - Authorize on the server. The claim decides in your backend, never in the browser. Anything the browser can compute, the browser can change.
- Nested groups need no extra code. A user who qualifies through a nested group receives the GUID of the rights group in the claim. Your code sees exactly what it would see for a direct member.
- A membership change takes effect with the next token. After an add or a remove, the user must sign in again or refresh their token. Grouper shows the change instantly; your application sees it later. This is the single most frequent "the permission did not apply" report, and it is working as designed.
- Handle the overage case if you want robustness. Above roughly 150 groups
Entra replaces the list with a Graph link (
_claim_names/_claim_sources). With a handful of application groups this is practically irrelevant, but it is the one shape your parser can meet and not recognise.
The machine API#
Grouper serves a small API under /api/v1 so an application can fetch its own
configuration and verify its own tokens at runtime, instead of carrying them in
its environment. This section is an orientation; the authoritative,
machine-readable contract is served, unauthenticated, at:
https://grouper.tokada.io/api/v1/docsPoint your agent or your integration at that URL rather than at a copy. It needs no API key and no login, because a coding agent sitting in a foreign repository has to be able to read how credentials work before anybody has handed it one. Grouper is on the internal network, so that fetch has to happen from the VPN.
Prove the key works before writing any code#
export GROUPER_BASE_URL=https://grouper.tokada.io
export GROUPER_API_KEY=<the key you were given>
curl -sS -w '\n%{http_code}\n' \
-H "Authorization: Bearer $GROUPER_API_KEY" \
"$GROUPER_BASE_URL/api/v1/rights"200 and you are done. A 401 or a 403 means a Tool-Admin has to re-issue the
key — there is nothing in your code to work around, and code written against an
unverified key is untested code shipped as if it were tested.
Read the error.code, not only the status: 403 forbidden_client means the key
is real but lacks the client role, while 403 token_expired means it is real
and expired or revoked. A 503 does not implicate the key at all.
Endpoints#
| Endpoint | What it does |
|---|---|
GET /api/v1/tokens | Hands you the hash list of every token your app publishes, so you can verify end-user tokens offline. |
POST /api/v1/validate | Answers, online, whether one end-user token is valid right now and which rights it carries. |
GET /api/v1/secrets/{name} | Hands you the plaintext of one provider secret your app has explicitly opted in for. |
GET /api/v1/rights | Hands you your app's own rights catalogue, and how many published tokens carry each name. |
POST /api/v1/entra | Hands you your own app's Entra configuration — tenant id, client id, client secret and, if you ask, the redirect URIs registered for the origin you were reached under. |
GET /api/v1/docs | Serves the full contract as Markdown, unauthenticated and cacheable. |
POST /api/v1/mailbox-access/check | Answers whether a user holds FullAccess on a shared Exchange Online mailbox — a single boolean, fail-closed, never an enumeration channel. |
POST /api/v1/mailbox-access/grant | Grants that access, scoped by a scopeGroup name belonging to your own application. |
Four properties of the whole surface are worth internalising:
- Scope is structural, not a parameter. Grouper hashes your bearer token,
finds the vault item it belongs to, and takes that item's
appKeyas the scope of the entire answer. No request field widens it, which is why cross-app access is impossible rather than forbidden. Another app's token simply answersvalid: false, reason: "unknown", and another app's secret name answers404, byte-identically to values that never existed — anti-oracle measures, not accidents. - This is not a browser API. No route sends CORS headers, every response
carries
Cache-Control: private, no-store, max-age=0, must-revalidate, and every body is credential-tier material. Call it from your server. /api/v1/entrais aPOST. AGETanswers405withAllow: POST. The body carries a selector (the origin), and a selector must not end up in a proxy access log, aReferer, or a cache key.- The docs route is the one exception to all of the above — it hands out no
credential, needs no
Authorizationheader, and is the one route answeringCache-Control: public.
Errors, caching and rate limits#
Every failure on every route has the same body shape:
{ "error": { "code": "invalid_request", "message": "The request body is invalid." } }Switch on code, never parse message. The message is fixed text for a
human reading a log, and a caller-supplied value is never echoed into it. The
codes deliberately withhold detail in several places — a 401 reads identically
for a missing header and a wrong one, and a 400 on a body never says what was
wrong with it, because that body carries a live credential.
The rule that matters most in production:
| Status | What to do |
|---|---|
429, 503 | Retry with backoff, respect Retry-After, keep serving from your cache. Never invalidate credentials or mark a user's token bad. |
401, 403 | Stop. Your own key is wrong or expired. Reload configuration and escalate; do not retry in a loop. |
400, 404, 405, 409 | Your request is wrong. Fix it; retrying is pointless. |
500 | Retry cautiously, then escalate. |
The failure this prevents: a Grouper outage causes your client to record "this user's token is invalid", and that rejection outlives the outage because you cached it. You cannot take a rejection back.
Rate limits are per token bucket, keyed on your client key's vault item rather
than on your IP, and each endpoint has its own bucket so a busy afternoon of
validation traffic cannot starve the list refresh that keeps revocations timely.
The shipped defaults are 60/minute for the token list and for rights, 600/minute
for validate, and 10/minute burst-free for secrets and for the Entra route. Two
caveats the contract states explicitly: the numbers are operator-configurable, so
Retry-After is authoritative and the table is not; and the limiter is
in-process per container, so with several replicas the effective limit multiplies.
Only Retry-After is returned. There are no X-RateLimit-* headers on any
route — do not write code that reads them.
Why Grouper instead of rolling your own auth#
This section is the argument, and it is deliberately written as mechanics rather than benefits. Every claim below names the specific thing that makes it true, and the last two subsections say what Grouper does not do for you.
You store no user records, so you cannot leak them#
An application wired to Entra through Grouper holds no user table, no password hashes, no password-reset tokens and no e-mail-verification state. It holds a client id, a client secret, and a set of group GUIDs. The entire class of incidents that begins with "our user table was exfiltrated" has no target, because the credential material lives in the identity provider and Grouper itself is stateless — it maintains no database at all.
You inherit MFA, Conditional Access and device compliance instead of rebuilding them#
Multi-factor enrolment, risk-based sign-in, "only from a compliant device", "not from this country", session lifetime, revocation on employment change — these are policies your tenant already runs, and a Grouper-created registration is a normal single-tenant registration inside that tenant, so they apply to your application on the day it is created. There is no code in your repository that has to implement, test or update any of them. Compare that with what a self-built login would owe you before it is even at parity.
Joiner–mover–leaver is solved where it is actually solved#
When somebody leaves, HR-driven offboarding disables the account in the
directory, and every application that trusts the directory loses them. If instead
your application maintains its own accounts, the leaver stays until someone
remembers to remove them from your application specifically. Grouper does not
add a second population of accounts to remember: rights are expressed as
memberships of directory groups, and nesting an existing organisational group
(All-Sales-EMEA) inside a rights group means the department's own lifecycle
moves your permissions for free.
One audit trail, in the place auditors already look#
Group membership changes, app-registration changes and role assignments land in
the Entra audit log, and they land there whether the change was made in Grouper
or in the portal — because Grouper does not shadow those objects, it writes them.
There is no second history to reconcile. The credential exits Grouper does
own are recorded on top of that: every reveal of a stored secret, whether through
the interface or through GET /api/v1/secrets/{name}, emits the same structured
event into one queryable stream, recording who, which app and which item — never
the value.
A deliberately narrow service identity#
Grouper's own App-Only identity holds exactly four application permissions:
Group.ReadWrite.AllUser.Read.AllApplication.ReadWrite.OwnedByRoleManagement.Read.Directory
And, more importantly, three permissions it deliberately does not hold:
- no
Application.ReadWrite.All— as a service it can only touch app registrations it owns, which is why Grouper is added as an owner at creation time; - no
RoleManagement.ReadWrite.Directory— it can read who holds a directory role, so it can recognise a Tool-Admin, and it can never grant one; - no
Directory.ReadWrite.All— there is no blanket write over the tenant.
One nuance stated honestly rather than glossed over: the delegated scope set
does include Application.ReadWrite.All, used when a signed-in Tool-Admin
creates or renames an app registration. A delegated scope grants nothing the
signed-in person does not already hold — Azure's own role check remains the
boundary and the tool escalates nothing. The application permission, which would
apply with no human attached, is the one that must never be granted.
Secrets have a single, one-way exit#
A rotated client secret is displayed once and is never written to a log, a
database or an e-mail; Grouper cannot recover it, only replace it. Inside the
credential inventory the same discipline is structural rather than promised:
exactly one function returns a plaintext, the raw vault item never leaves its
module, and the reveal dialog holds the value in transient local state that is
dropped on close. On the machine side, the secrets endpoint deliberately has
no ETag — an ETag over a plaintext is a stable fingerprint of the
credential, readable at every TLS-terminating hop, and would become a rotation
oracle. Since 2026-08-30 the delivered value comes from a 15-minute cache held
in Grouper's process memory only (never disk, never a log line): a rotation
done in Grouper is visible to consumers immediately, an edit made directly in
1Password after at most 15 minutes. A consumer whose credential was just
rejected by the provider can append ?refresh=1 to force a live vault read
— throttled to one forced read per item per minute, and when the forced read
returns an unchanged value, Grouper files a Jira task asking an admin to check
the credential itself, because re-fetching cannot fix it.
Every boundary is enforced on the server#
Three guards run before any write: the admin-role check, the app-membership check, and the prefix check that a group name lies inside a namespace you manage. Hidden menu entries and missing tabs are cosmetics, not boundaries — every page re-checks on the server, and a non-member who guesses a URL gets nothing. The default everywhere is fail-closed. This is worth contrasting with the usual home-grown outcome, where the check that matters is the one in the component that renders the button.
What you still own#
Grouper removes a category of work. It does not remove these:
- Network reachability. Grouper is VPN-only and the internal network is a real part of the protection model. Keeping it that way is an operations task, not something the tool enforces about itself.
- Tenant hygiene. Conditional Access policies, MFA enrolment coverage, who holds an application-administrator role, and whether those roles are PIM-gated are decisions in your tenant. Grouper inherits your posture; it cannot improve a bad one.
- Your GUID-to-right mapping and its enforcement. Grouper carries names and
memberships. What
teamleadmay do in your application is your code, on your server, and the rules in Wiring SSO into your application — especially "filter to your own groups" — are yours to get right. - Token refresh behaviour. Your session lifetime decides how long a removed permission keeps working. Grouper cannot revoke a token it never issued.
- Your own operational alarms. If you consume the machine API, watch for a
sustained
stale: trueand for the snapshot age climbing on/api/health. Those are alarms on your side. - The secret in your configuration. Grouper hands it over once; storing it, keeping it out of Git, and rotating on schedule are yours.
When Grouper is the wrong tool#
Say so early rather than bending the model:
- External or customer identities. Grouper manages security groups in your corporate tenant, and every subject it can name is an account in that directory. Customer sign-up, self-registration, social login and B2C flows are a different product — Entra External ID or an equivalent — and modelling customers as tenant users to fit Grouper is a mistake that is very hard to reverse.
- Applications not backed by this Entra tenant. If sign-in comes from
another identity provider, there are no group GUIDs for Grouper to hand you,
and the machine API's scoping — which reads an
appKeyoff a vault item — has nothing to attach to. - Per-record or per-tenant authorization. "May Alice edit this document" is not a group membership. Group claims are coarse and arrive in a token whose size has an actual limit (the overage case above starts at roughly 150 groups). If your permission model multiplies with your data, you need an authorization layer in your application, and Grouper should carry only the coarse roles that feed it.
- Sub-second permission changes. Rights take effect with the next token. If your requirement is immediate revocation, you need an online check — the validate endpoint for machine tokens, or your own session invalidation for users — not a group membership.
Operations and security#
The network is part of the model#
Grouper is internal and VPN-only. This is stated as a protection layer and not as an inconvenience: the prefix restriction, the role checks and the membership checks all live in application code, and the network keeps the surface those checks run on out of reach of the public internet.
Authorization is server-side, always#
Restating the point because it decides how you should read the interface: a hidden menu item, an absent tab and a disabled button are presentation. The server re-checks the admin role, the manager membership and the group-name prefix on every write, and refuses by default.
Error codes and incident IDs#
Every error surfaced in the interface carries two things: a stable code of
the form GRP-<AREA>-<NNNN> (for example GRP-APP-0104) and a six-character
incident ID (for example 7K2F9A).
- The code groups similar failures — it is per area and cause, so support can recognise a class of problem across many reports.
- The incident ID connects exactly one message on your screen to exactly one line in the server log. It is minted on the server only, and it is derived from nothing — it is not a hash of the message, because an ID you can reproduce would be an oracle for server-side text.
When you report a problem, quote both. They are the only things that make the server-side line findable. Grouper also offers a report dialog that files the report as a Jira task with the code and ID attached. Codes are append-only and are never recycled, so the history stays trustworthy.
If you ever see the fallback code GRP-UI-0000, the failure did not travel
through the normal path — mention that too.
Every code is listed with its cause and its instruction under Error code reference. In the interface the code itself is a link straight to its entry.
Eventual consistency, and what Grouper does about it#
Microsoft Graph is eventually consistent: a list request issued immediately after a write frequently returns the state from before the write. Grouper therefore applies your change to the view in front of you immediately and reconciles in the background against Graph, preferring a strongly consistent read-by-id for an item you just created or edited.
For you this means: the interface is right and Graph may lag. If a just-created group does not appear in some other tool for a few seconds, nothing went wrong. It also means the mail-mirror indicator, which depends on an Exchange object built by a nightly run, can legitimately disagree with the switch you flipped this morning.
Readiness, not liveness#
GET /api/health is a readiness probe, not a liveness probe. A 503 with
reason: "snapshot_unavailable" usually means a new instance is warming up while
the running one still serves traffic. It is a signal not to switch over yet —
not an outage, and not a reason to page anyone.
FAQ and troubleshooting#
| Symptom | Cause and fix |
|---|---|
| I do not see my application under Applications. | You are not a member of Grouper|<AppKey>|Manager. Ask a Tool-Admin, then sign out and back in — a membership only appears in a newly issued token. |
| A user does not see their new permission. | Their token predates the change. Sign out and in, or refresh the token. Grouper shows the change instantly; your application sees it at the next token. |
AADSTS50011: redirect URI mismatch | The URI in Grouper does not match your origin exactly. Compare scheme, host, port, path and trailing slash character by character. |
The groups claim is missing entirely. | The application was not created through Grouper and lacks groupMembershipClaims. A Tool-Admin has to fix the registration. |
The group name Manager is rejected. | It is reserved — it would collide with the manager group and grant manager privileges. Choose another name. |
A name containing | or # is rejected. | The pipe is the grammar's separator, and # marks a mail-enabled mirror. Both are structural, not stylistic. |
| I lost the client secret. | Do not search for it. Rotate. Grouper stores it nowhere, and rotation invalidates the old value immediately. |
My API calls return 403 forbidden_client. | Your key does not carry the client role — most often because somebody handed you an end-user token from the published list. Only a Tool-Admin can issue a client key. |
My API calls suddenly return 401 on a deployment that worked. | The key was rotated; there is no overlap window. Get the new value. It is read at runtime, so no rebuild is needed. |
404 entra_not_configured. | Not an authentication problem. Your key is fine; Entra delivery is not switched on for the app. A Tool-Admin does that on the app's Application tab. |
404 redirect_uri_not_registered. | Also not an authentication problem. When you report it, quote the origin your code computed — Grouper never logs it, so your log is the only place it exists. |
| The contract URL does not resolve. | You are off the VPN. https://grouper.tokada.io/api/v1/docs is internal like everything else. |
| The mail-mirror indicator has not appeared. | The mirror is built by the nightly run. Until then the opt-in is real and the Exchange object is not — the dialog says exactly that. |
| A Tool-Admin says they are not admin. | Their directory role is PIM-eligible but not activated. Grouper needs the role active. |
Reference#
Grammar cheat sheet#
| Thing | Shape | Notes |
|---|---|---|
| Manager group | Grouper|<AppKey>|Manager | Membership makes you a manager of the app. |
| Rights group | Grouper|<AppKey>|<name> | Its object id appears in your users' groups claim. |
| App-scoped prefix | Grouper|<AppKey>| | The server-side write boundary. |
| AppKey | any Azure-legal name, 1–100 chars | No |, no leading or trailing whitespace. Spaces, hyphens, mixed case and umlauts are fine. |
| Rights-group name | any Azure-legal name, 1–100 chars | No |, no #, no edge whitespace. Manager is reserved. |
| Right | area.action, lowercase | Dot mandatory, ≤ 64 chars, ≤ 64 per app. |
| Mail mirror | <source>#MailEnabled | Machine-owned; membership overwritten on every sync. |
| Client key | 64 lowercase hex characters | No prefix on the bearer token itself. |
| Token hash / list version | sha256:<64 lowercase hex> | Appears on hashes and ETags, never on the bearer token. |
| Error code | GRP-<AREA>-<NNNN> | Append-only, never recycled. |
| Incident ID | 6 characters | Server-minted, quote it in every report. |
Glossary#
- AppKey — the short key of an application; the middle segment of every managed group name.
- App manager — a member of
Grouper|<AppKey>|Manager; administers that one application. - Tool-Admin — a holder of an active Entra Application Administrator or Cloud Application Administrator role; administers Grouper itself.
- Rights group — a security group under an application's prefix, whose object id becomes a right in that application's tokens.
- Right — a lowercase
area.actionname the application defines and interprets; Grouper carries it and never interprets it. - Client key — a
client-role credential that lets a machine call/api/v1; its vault item'sappKeyis the scope of every answer. - Published token — a credential whose hash Grouper hands to its owning application so the application can verify it offline.
- Fetchable secret — a stored provider secret explicitly opted in, per item,
for retrieval through
GET /api/v1/secrets/{name}. The response carries the plaintext plus the item's username and custom fields — one opt-in covers all of them, so keep unrelated secrets out of a fetchable item's custom fields. - Open Extension — the Graph mechanism Grouper uses to hang an application's metadata on its manager group, instead of keeping a database.
- Mail mirror — the one-way Exchange Online copy of a rights group, needed
because
ApplicationAccessPolicyaccepts only mail-enabled groups. - Incident ID — the six-character identifier connecting one error message to one server log line.
Links#
- Live machine-API contract:
https://grouper.tokada.io/api/v1/docs— always current, needs no credentials, reachable from the VPN. - Health / readiness probe:
https://grouper.tokada.io/api/health. - Everything else — the living specification, the frozen product baseline and the agency-wide Entra playbook — lives in the repository and is maintainer-facing.
Error code reference#
Every error Grouper shows you carries a code of the form GRP-<AREA>-<NNNN>.
This is the full list: what each one means, and — word for word — the same
instruction the interface gave you. Codes are append-only and are never
recycled, so an entry here keeps meaning what it meant when it was written.
The incident ID shown next to the code is not listed here and cannot be: it is minted per failure and resolves to exactly one line in the server log. Quote both when you report a problem.
Applications (GRP-APP-01xx)#
GRP-APP-0101#
Your account lacks the directory privilege the application action needs — the Application Administrator role was not active on your session.
What to do: Ask a Tool-Admin to run this for you, or activate your Application Administrator role in PIM and sign in again.
GRP-APP-0102#
Your delegated session had expired by the time the action reached the server.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-APP-0103#
The server refused a value in your input. Nothing was written.
What to do: Correct the value the message names and submit again. The input was refused before anything was written, so nothing needs undoing.
GRP-APP-0104#
A Graph or network failure Grouper could not attribute to a known cause.
What to do: Retry once. If it fails again, report it with the incident ID — that ID is what makes the server log line findable.
GRP-APP-0105#
Azure had not finished replicating the object the write depended on.
What to do: Wait about ten seconds and repeat the same action. The object exists and will become visible — do not create it a second time.
GRP-APP-0106#
Microsoft Graph rate-limited the request (HTTP 429).
What to do: Nothing is misconfigured. Avoid repeating the action in a tight loop: every extra request extends the throttling window.
GRP-APP-0107#
Azure refused the write because an object with that name already exists (HTTP 409).
What to do: The object holding that name may sit outside the managed prefix, where Grouper can neither show nor remove it. Search the directory in Azure.
GRP-APP-0108#
Deleting an application was stopped before anything in Azure was touched, because at least one of the application's 1Password items could not be deleted. The application, its groups and its app-registration are all still there.
What to do: Check the 1Password service account, then delete the application again.
GRP-APP-0109#
The application list behind the header's switcher could not be read, because your account lacks the directory privilege that read needs.
What to do: Ask a Tool-Admin to run this for you, or activate your Application Administrator role in PIM and sign in again.
GRP-APP-0110#
Your delegated session had expired when the switcher tried to load its list.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-APP-0111#
The server refused a value while loading the switcher's list. Nothing was written.
What to do: Correct the value the message names and submit again. Nothing was written.
GRP-APP-0112#
The switcher's application list could not be loaded, for a reason the server could not classify. Nothing else on the page is affected.
What to do: Reopen the menu to retry. Until it loads, reach the application through the Applications overview.
Groups and members (GRP-GRP-02xx)#
GRP-GRP-0201#
You are not a manager of that application, or your session carries no directory privilege for the group.
What to do: Ask a Tool-Admin to add you to the application’s Manager group, then sign out and back in — Grouper reads your groups at sign-in.
GRP-GRP-0202#
Your delegated session had expired by the time the group action reached the server.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-GRP-0203#
The group name or member reference was refused. Nothing was written.
What to do: Correct the value the message names and submit again. The input was refused before anything was written, so nothing needs undoing.
GRP-GRP-0204#
A Graph or network failure on a group write that Grouper could not attribute to a known cause.
What to do: Reload the page and check whether the change landed anyway, then retry. If it fails a second time, report it with the incident ID.
API keys and the 1Password vault (GRP-KEY-03xx)#
GRP-KEY-0301#
You are not a manager of that application, or the key carries a role you may not issue.
What to do: Ask a Tool-Admin to add you to the application’s Manager group, or to issue the key for you if it carries a privileged role.
GRP-KEY-0302#
Your delegated session had expired by the time the key action reached the server.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-KEY-0303#
The key name, role or rights list was refused. No key was created, rotated or deleted.
What to do: Correct the value the message names and submit again. No key was created, rotated or deleted, so nothing needs undoing.
GRP-KEY-0304#
A failure on a key path that did not come from the 1Password vault — Azure or the network.
What to do: The vault is fine; this failure came from Azure or the network. Retry once, then report it with the incident ID.
GRP-KEY-0305#
A 1Password failure that matched none of the recognised causes below. It is the rest, not the default.
What to do: Retry once. If it fails again, report it with the incident ID — that ID is what makes the server log line findable.
GRP-KEY-0306#
The 1Password service account exhausted its vault-operation quota.
What to do: Nothing is misconfigured and no data was lost — the service account ran out of vault-operation quota.
GRP-KEY-0307#
1Password rejected the service-account token — expired or revoked.
What to do: The service-account token has expired or was revoked. An operator must issue a new token and restart Grouper.
GRP-KEY-0308#
The configured vault was not found for the service account: renamed, or its access was withdrawn.
What to do: Either the vault no longer exists or the service account lost access. Check the vault selected in Settings against 1Password, then re-grant read+write.
GRP-KEY-0309#
1Password could not be reached at all — network, DNS or TLS.
What to do: This is a network fault between Grouper and 1Password, not a configuration error. Retry in a minute; if it persists, check outbound access.
GRP-KEY-0310#
The 1Password client could not be started at all, before any request was made.
What to do: This is a deployment fault. Report it with the incident ID; restarting the instance is the usual fix, and only an operator can do that.
Client secrets (GRP-SEC-04xx)#
GRP-SEC-0401#
You are not a manager of that application, so you may not rotate its secrets.
What to do: Ask a Tool-Admin to add you to the application’s Manager group, then sign out and back in before rotating a secret.
GRP-SEC-0402#
Your delegated session had expired by the time the secret action reached the server.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-SEC-0403#
The requested lifetime or secret name was refused. No secret was created or removed.
What to do: Correct the value the message names and submit again. No secret was created or removed, so nothing needs undoing.
GRP-SEC-0404#
A Graph or network failure during a secret operation that Grouper could not attribute to a known cause.
What to do: Reload the application and check its secret list before you rotate again — a failed rotation can still have created a secret.
GRP-SEC-0405#
Reserved and unreachable by design: the only credential-store call on this path never throws — it reports a vault failure on an otherwise successful result instead.
What to do: No rotation path can produce this code, so seeing it is itself the defect. Report it with the incident ID so the branch that minted it can be found.
Entra configuration delivery (GRP-EAPI-05xx)#
GRP-EAPI-0501#
You are not a manager of that application, so you may not deliver its Entra configuration.
What to do: Ask a Tool-Admin to add you to the application’s Manager group, or to run the delivery step for you.
GRP-EAPI-0502#
Your delegated session had expired by the time the delivery reached the server.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-EAPI-0503#
The delivery input was refused. Nothing was handed to the consuming application.
What to do: Correct the value the message names and submit again. Nothing was delivered, so the consuming application still sees its old configuration.
GRP-EAPI-0504#
The configuration delivery failed for a reason Grouper could not attribute.
What to do: The consuming application keeps its current configuration. Retry once, then report it with the incident ID.
Settings (GRP-SET-06xx)#
GRP-SET-0601#
Global settings are Tool-Admin only, and your session carried no active directory role.
What to do: Global settings are Tool-Admin only. Ask a Tool-Admin to make the change, or activate your directory role in PIM and sign in again.
GRP-SET-0602#
Your delegated session had expired by the time the settings write reached the server.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-SET-0603#
A settings value was refused. The stored settings are unchanged.
What to do: Correct the value the message names and submit again. The form still holds what you typed, so nothing has to be re-entered.
GRP-SET-0604#
The settings file could not be written for a reason Grouper could not attribute.
What to do: The previous values still apply — nothing was half-written. Retry once, then report it with the incident ID.
GRP-SET-0605#
Listing the 1Password vaults requires an active directory role your session did not carry.
What to do: Choosing the 1Password vault is Tool-Admin only. Ask a Tool-Admin to make the change, or activate your directory role in PIM and sign in again.
GRP-SET-0606#
Your delegated session had expired by the time the vault listing reached the server.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-SET-0607#
Reserved: the vault listing takes no input, so nothing can be refused here. Seeing this code means something unexpected happened.
What to do: The vault listing takes no input, so this code should be unreachable. Report it with the incident ID if you see it.
GRP-SET-0608#
The 1Password vaults could not be listed for a reason Grouper could not attribute.
What to do: The saved vault selection still applies. Retry once; if it fails again, 1Password itself is likely unavailable and only waiting helps.
Jira: lookup, error reports and health (GRP-JIRA-07xx)#
GRP-JIRA-0701#
The Jira lookup requires an active directory role your session did not carry.
What to do: Ask a Tool-Admin to run the lookup for you, or activate your directory role in PIM and sign in again.
GRP-JIRA-0702#
Your delegated session had expired by the time the Jira lookup reached the server.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-JIRA-0703#
The lookup input was refused. Nothing was written to Jira.
What to do: Correct the value the message names and submit again. Nothing was written to Jira.
GRP-JIRA-0704#
Jira could not be reached, or answered in a way Grouper could not attribute.
What to do: Check the Jira base URL and credentials under Settings first; if they are correct, Jira itself is unavailable and only waiting helps.
GRP-JIRA-0705#
Reserved and expected to stay unreachable: filing an error report is open to every signed-in user, so no authorization guard can fire.
What to do: Filing a report is open to every signed-in user, so this code should be unreachable. Copy this message and send it on by hand.
GRP-JIRA-0706#
Your delegated session had expired by the time the error report reached the server.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-JIRA-0707#
The report summary, description or incident id was refused.
What to do: Shorten or complete the field the message names and submit again. The original error is still on screen and can be copied instead.
GRP-JIRA-0708#
Jira accepted no task for this report — unreachable, or it refused the issue.
What to do: Copy this message and send it on by hand — nothing is waiting in Jira for someone to find.
GRP-JIRA-0709#
Error reporting has no Jira project configured, so there is nowhere to file the report.
What to do: Ask a Tool-Admin to fill in the Jira project and credentials under Settings. Until then, copy this message and send it on by hand.
GRP-JIRA-0710#
The Jira diagnostics page requires an active directory role your session did not carry.
What to do: Ask a Tool-Admin to run the diagnostics page, or activate your directory role in PIM and sign in again.
GRP-JIRA-0711#
Your delegated session had expired by the time the health check reached the server.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-JIRA-0712#
Reserved and expected to stay unreachable: the health-check action takes no parameters, so it has no input it could refuse.
What to do: The health check takes no input, so this code should be unreachable. Report it with the incident ID if you see it.
GRP-JIRA-0713#
The Jira health check itself failed to complete. That is not the same as Jira being down.
What to do: Run the check once more. If it fails again, report it with the incident ID — a failing check is not itself proof that Jira is down.
GRP-JIRA-0714#
You ran the health check more often than the per-user budget allows.
What to do: The budget is per user and refills on its own. Nothing about Jira changed, and the last result on the page is still valid.
Expiry scan (GRP-CRON-08xx)#
GRP-CRON-0801#
Starting the expiry scan requires an active directory role your session did not carry.
What to do: Ask a Tool-Admin to start the scan, or activate your directory role in PIM and sign in again.
GRP-CRON-0802#
Your delegated session had expired by the time the scan request reached the server.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-CRON-0803#
The scan could not start because the settings it depends on are incomplete.
What to do: Correct the value the message names and start the scan again. No notification was sent.
GRP-CRON-0804#
The expiry scan failed for a reason Grouper could not attribute.
What to do: A full scan also runs on its own daily schedule, so this is not the only chance. If you need the result now, retry once, then report it with the incident ID.
Mail mirrors (GRP-MIRR-09xx)#
GRP-MIRR-0901#
You are not a manager of that application, so you may not change its mail mirror.
What to do: Ask a Tool-Admin to add you to the application’s Manager group, then sign out and back in before changing the mirror.
GRP-MIRR-0902#
Your delegated session had expired by the time the mirror action reached the server.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-MIRR-0903#
The mirror opt-in value was refused. The mirror keeps its current state.
What to do: Correct the value the message names and submit again. The mirror keeps its current state until the request is accepted.
GRP-MIRR-0904#
Reading or writing the mirror opt-in failed for a reason Grouper could not attribute.
What to do: Reload the group and check whether the opt-in shows what you wanted, then retry once. If it fails again, report it with the incident ID.
n8n credentials (GRP-N8N-10xx)#
GRP-N8N-1001#
You are not a manager of that application, so you may not manage its n8n credentials.
What to do: Ask a Tool-Admin to add you to the application’s Manager group, then sign out and back in before managing its n8n credentials.
GRP-N8N-1002#
Your delegated session had expired by the time the n8n action reached the server.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-N8N-1003#
The input was refused before anything was written — neither the n8n instance nor the vault changed.
What to do: Correct the value the message names and submit again. Nothing was written to n8n or to the vault.
GRP-N8N-1004#
Grouper could not reach the n8n instance — a network, DNS or TLS failure on the way there. The stored configuration is unchanged.
What to do: Check that the configured base URL is reachable from the server, then retry once — the stored configuration is unchanged.
GRP-N8N-1005#
The instance's public API does not support credential updates, so activation was refused before anything was created.
What to do: Ask the instance operator to upgrade n8n to a version whose public API supports credential updates, then activate again.
GRP-N8N-1006#
The new value is already deployed in n8n, but writing it to the vault failed — the two stay out of step until a rotation succeeds.
What to do: Repeat the rotation — the step is idempotent, and repeating it brings the vault back in line with the value already deployed in n8n.
GRP-N8N-1007#
The instance rejected Grouper's API key (401), so the request never reached the credential.
What to do: Rotate the instance API key in n8n, store the new key by activating again, then repeat the action.
GRP-N8N-1008#
The instance reported a conflict or an ownership violation for the object the action addressed.
What to do: Reload the credential list so it reflects the instance, then repeat the action on the current state.
GRP-N8N-1009#
The instance rate-limited the request or answered with a server error, and the retries Grouper already performed did not get through.
What to do: Wait a minute before retrying — the instance is throttling or failing upstream, and rapid retries extend the window.
User directory (GRP-USR-11xx)#
GRP-USR-1101#
The user directory could not be built — reading the Grouper groups or their members failed — and no earlier snapshot existed to fall back to.
What to do: Retry once — the directory is rebuilt from every Grouper group, so one flaky read fails the whole load. Then report it with the incident ID.
GRP-USR-1102#
A manually requested refresh of the user directory failed, and no earlier snapshot existed to fall back to. When one exists, the refresh keeps showing it instead of raising this error.
What to do: Wait a moment and refresh once more. If it keeps failing, report it with the incident ID.
GRP-USR-1103#
The user directory is Tool-Admin only, and your session carried no active directory role.
What to do: The user directory is Tool-Admin only. Ask a Tool-Admin to look it up, or activate your directory role in PIM and sign in again.
GRP-USR-1104#
Your delegated session had expired by the time the directory request reached the server.
What to do: Sign in again, then reopen the user directory — nothing you did was lost, the directory is read-only.
GRP-USR-1105#
The request was refused before any directory read started. Reserved: the directory actions currently accept no input that could be refused this way.
What to do: Correct the value the message names and try again. The request was refused before any directory read started.
Shared items (GRP-SHARE-12xx)#
GRP-SHARE-1201#
The shared-item action was refused because your session carried no active Tool-Admin role. Shared items live in the reserved SHARED scope and are administered by Tool-Admins only.
What to do: Shared items are Tool-Admin territory. Ask a Tool-Admin to make the change, or activate your directory role in PIM and sign in again.
GRP-SHARE-1202#
Your delegated session had expired by the time the shared-item request reached the server.
What to do: Copy anything you typed in an open dialog first — signing in reloads the page and discards it.
GRP-SHARE-1203#
The input was refused before anything was written — a missing or colliding API name, an invalid value, an app key that is not a valid application (a reserved scope, say), or a target that is not a shared item. The message names the value.
What to do: Correct the value the message names and submit again. Nothing was written to the vault.
GRP-SHARE-1204#
A shared-item operation failed for a reason the server could not classify further.
What to do: Retry once. If it fails again, report it with the incident ID — the shared inventory keeps its previous state.
GRP-SHARE-1205#
Reserved — no longer issued. Older versions used it when converting a key into a shared item stopped halfway; the conversion is now a single write, so this partial state cannot occur any more. You may still see the code on old incident reports.
What to do: Run the conversion again — the retry is safe and completes in one step.
Credential-store write lock (GRP-VLOCK-13xx)#
GRP-VLOCK-1301#
Grouper runs on more than one instance, and all of them share a single credential vault. Only the instance that owns the vault is allowed to change it; every other instance reads the vault normally but refuses to write to it. The refusal happens before anything is sent to the vault, so nothing was created, changed or deleted.
What to do: Repeat the change on the Grouper instance that owns the credential vault — an administrator can tell you which one that is.
The fallback (GRP-UI-0000)#
GRP-UI-0000#
The floor. The failure never travelled through the server-side error path, so the identifier next to it was minted in your browser and matches no server log line.
What to do: Reload the page and try once more. If it happens again, report it with the incident ID and say that the code was GRP-UI-0000.