applications
The application catalog behind the "My apps" launcher: the tools an organization gives its people, and who has each.
The application catalog behind the "My apps" launcher: the tools an organization gives its people, and who has each.
An app is an OpenID Connect client of this deployment (oauthClientId, kind oidc) or a plain link to any other tool
(kind link). Administrators give each app to everyone or assign it to people and groups, optionally until a date;
people open their apps from the launcher, every launch is recorded and audited, and people without an app can request
it through an access package. For an oidc app the built-in OAuth provider enforces the assignments: through the
clientAllowed hook iam.protocolHost supplies, it refuses to issue or refresh tokens for people without the app. See
the application catalog guide; the repository guide is
docs/applications.md.
Who has an app
A person has an app when it is enabled and its visibility is everyone, or it is assigned to them, or it is assigned
to a group they belong to, counting only assignments and memberships that have not ended. Only active, unexpired
people have apps: service accounts, agents, and disabled, expired or deleted accounts have none. via says why: direct, else group,
else everyone. Assignments are kept whatever the visibility, so switching an app to assigned leaves exactly the
assigned people and groups with it.
Permissions
iam:applications:read covers list, listAssignments, usage and check; iam:applications:manage covers
create, update and delete; iam:applications:assign covers assign, unassign and removeUnused. Actions are
checked on iam/TENANT_ID for create, list, usage, check and listAssignments without appId, and on the
app for update, delete, assign, listAssignments with appId, removeUnused and unassign (the assignment's
app). People's email addresses and names appear in listAssignments and usage only for callers also allowed
iam:identities:read on the tenant. mine and launch need no permission, only a person's own signed-in session of
the tenant. The deployment's own code checks access with iam.applications.allowed, which takes no credential and is
not audited (see check). iam.sweepExpired() deletes assignments past their end, and deleting a person or
a group removes their assignments.
| Method | What it does | Access |
|---|---|---|
assign | Gives an app to a person or a group, optionally until expiresAt. | Credential |
check | Tells whether a person may use an app, by appId or by the app's oauthClientId, for sign-in pages that enforce assignments. | Credential |
create | Registers an app in the tenant's catalog: an OAuth client of this deployment, or a link to any tool. | Credential |
delete | Deletes an app with its assignments and launch history. | Credential |
launch | Opens an app the caller has: records the launch and returns the app's launch URL. | Credential |
list | Lists every app of the tenant by name, with how many live assignments each has and how many people opened it recently. | Credential |
listAssignments | Lists an app's assignments, or every app's, with who they name and when each person last opened the app. | Credential |
mine | Returns the caller's launcher: the apps they may open now and the apps they may request. | Credential |
removeUnused | Removes an app's direct assignments to people who have not opened it for unusedDays. | Credential |
unassign | Removes one assignment of an app. | Credential |
update | Changes an app's name, URLs, client, visibility, request package, owners, or whether it is enabled. | Credential |
usage | Reports, per app, how many people have it, how many opened it in 30 days, and the direct assignments nobody uses. | Credential |
assign
Gives an app to a person or a group, optionally until expiresAt.
Used inApplication catalog
- Permission:
iam:applications:assignon the app. - Audited as:
iam:applications:assign, plusapp:assignwith the app key,subjectType,subjectIdandexpiresAt. - Errors:
INVALID_INPUTfor asubjectTypeother thanidentityorgroup, an identity that is not a person (service accounts and agents), or anexpiresAtnot in the future or more than ten years away;NOT_FOUNDwhen the app, person or group is not in this tenant, or the person is deleted;LIMIT_EXCEEDED(409) for a new assignment to an app that already has 10,000 (assign groups instead).
Assigning the same person or group again replaces the assignment: the new expiresAt (none when left out) applies,
and assignedBy and assignedAt become the caller and now. An assignment stops counting the moment it ends. The
result is the assignment.
await iam.api.applications.assign(credential, {
tenantId,
appId,
subjectType: 'group',
subjectId: salesGroupId,
});Prop
Type
A AppAssignment object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/applications/assign" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"appId": "<appId>",
"subjectType": "identity",
"subjectId": "<subjectId>"
}'iam.api.applications.assign(
credential: CredentialInput,
input: {
tenantId: string;
appId: string;
subjectType: 'identity' | 'group';
subjectId: string;
expiresAt?: number;
},
): Promise<AppAssignment>check
Tells whether a person may use an app, by appId or by the app's oauthClientId, for sign-in pages that enforce assignments.
- Permission:
iam:applications:readon the tenant. - Audited as:
iam:applications:read. - Errors:
INVALID_INPUTwhen the call names both or neither ofappIdandoauthClientId;NOT_FOUNDfor an unknownappId.
The result is { allowed, governed, appId? }. An oauthClientId that no app names is not governed by the catalog:
{ allowed: true, governed: false }. For an app, governed is true and allowed says whether the person has it
now under who has an app; an identity that does not exist or belongs to another tenant, or an
organization that is not active, is not allowed. Disabled apps are found too, so a disabled app's client is refused for
everyone. The built-in OAuth provider asks the same question through its clientAllowed hook when it redeems codes,
refreshes tokens and answers userinfo, so it refuses people without the app at the token endpoint. Call this, or
iam.applications.allowed with the same input and no credential, in your OAuth login or consent page to refuse them
with an explanation before completing the interaction.
const { allowed } = await iam.applications.allowed({
tenantId: details.tenantId,
identityId: identity.id,
oauthClientId: details.clientId,
});Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/applications/check" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.applications.check(
credential: CredentialInput,
input: {
tenantId: string;
identityId: string;
appId?: string;
oauthClientId?: string;
},
): Promise<{ allowed: boolean; governed: boolean; appId?: string }>create
Registers an app in the tenant's catalog: an OAuth client of this deployment, or a link to any tool.
Used inApplication catalog
- Permission:
iam:applications:manageon the tenant. - Audited as:
iam:applications:manage, plusapp:createwith the key and the app's name, launch URL, visibility,enabled, client and request package. - Errors:
INVALID_INPUTfor a malformed key, a name over 120 characters, a description over 1000, a category over 60, a launch or logo URL that is not absolutehttps(orhttpon the local machine, while the deployment itself runs there) or carries a user name or password, anoauthClientIdthat is not a live OAuth client of the tenant, a package that is not requestable, more than 20 owners, an unknownvisibilityor a non-booleanenabled;NOT_FOUNDfor an unknown package or owner;CONFLICT(409) when an app has the key, or another app already governs theoauthClientId;LIMIT_EXCEEDED(409) past 500 apps.
key is permanent: 1 to 64 lowercase letters, digits, dots, underscores or hyphens, starting with a letter.
launchUrl is required: the app's sign-in URL, where launching sends the person. oauthClientId makes the app oidc
(otherwise link) and lets it decide who may sign in to that client; one app at most may name a client. visibility is assigned by default and enabled is true. requestPackageId names a
requestable access package that people without the app may request; it should grant a group the app is assigned to.
ownerIds names up to 20 identities to contact about the app and grants them nothing.
const app = await iam.api.applications.create(credential, {
tenantId,
key: 'crm',
name: 'CRM',
launchUrl: 'https://crm.example.com/login',
category: 'Sales',
oauthClientId: 'crm-web',
requestPackageId: salesToolsPackageId,
});Prop
Type
A Application object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/applications/create" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"key": "<key>",
"name": "<name>",
"launchUrl": "<launchUrl>",
"tenantId": "<tenantId>"
}'iam.api.applications.create(
credential: CredentialInput,
input: ApplicationInput & { tenantId: string },
): Promise<Application>delete
Deletes an app with its assignments and launch history.
- Permission:
iam:applications:manageon the app. - Audited as:
iam:applications:manage, plusapp:deletewith the key and the app's state. - Errors:
NOT_FOUNDwhen the app is not in this tenant;CONFLICT(409) when the app governs an OAuth client andreleaseClient: trueis not given.
The assignments go without an app:unassign event each. An app with an oauthClientId decides who may sign in to
that client, and without it everyone of the organization may, so deleting it needs releaseClient: true. To take an
app off every launcher but keep its assignments, and keep refusing its client, set enabled: false with
update. The result is { deleted: true }.
await iam.api.applications.delete(credential, { tenantId, appId, releaseClient: true });Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/applications/delete" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"appId": "<appId>"
}'iam.api.applications.delete(
credential: CredentialInput,
input: { tenantId: string; appId: string; releaseClient?: boolean },
): Promise<{ deleted: boolean }>launch
Opens an app the caller has: records the launch and returns the app's launch URL.
- Permission: None beyond a person's own signed-in session of the tenant.
- Audited as:
app:launchwith the app key. - Errors:
ACCESS_DENIED(403) when the app is not the caller's (unassigned, disabled, or unknown), and for API keys, role sessions, session tokens, delegated sessions, or a session of another tenant;IMPERSONATION_RESTRICTED(403) while an administrator views as the person.
Each person's launches of an app are kept as one record: the first and last time and how many. The result is
{ url }; open it yourself. Launching does not sign the person in to the app: its own sign-in runs as usual. In a
browser, open the tab during the click and point it at the URL once the call returns, since browsers block tabs
opened later.
const { url } = await client.applications.launch({ tenantId, appId });Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/applications/launch" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"appId": "<appId>"
}'iam.api.applications.launch(
credential: CredentialInput,
input: { tenantId: string; appId: string },
): Promise<{ url: string }>list
Lists every app of the tenant by name, with how many live assignments each has and how many people opened it recently.
- Permission:
iam:applications:readon the tenant. - Audited as:
iam:applications:read.
Disabled apps are included. Each app carries its fields plus assignments (assignments that have not ended) and
launchedLast30Days (people whose last launch was within 30 days).
Prop
Type
An array of object.
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/applications/list" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.applications.list(
credential: CredentialInput,
input: { tenantId: string },
): Promise<{
assignments: number;
launchedLast30Days: number;
key: string;
name: string;
description?: string;
category?: string;
launchUrl: string;
logoUrl?: string;
kind: 'oidc' | 'link';
oauthClientId?: string;
visibility: 'everyone' | 'assigned';
enabled: boolean;
requestPackageId?: string;
ownerIds: string[];
createdAt: number;
updatedAt: number;
id: string;
tenantId: string;
uniqueKey?: string;
}[]>listAssignments
Lists an app's assignments, or every app's, with who they name and when each person last opened the app.
- Permission:
iam:applications:readon the app, or on the tenant whenappIdis left out. - Audited as:
iam:applications:read. - Errors:
NOT_FOUNDwhen the app is not in this tenant.
Without appId, the result holds the assignments of every app of the tenant (one call for a catalog page). Each
assignment carries appId, subjectType, subjectId, assignedBy, assignedAt, expiresAt, subjectName and,
for people, lastLaunchedAt, sorted by subjectName (else subjectId). subjectName is the group's name, or the
person's email address or name for callers also allowed iam:identities:read on the tenant. It lists every stored
assignment, including ones past their end that the sweep has not removed yet.
Prop
Type
An array of AssignmentView.
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/applications/listAssignments" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.applications.listAssignments(
credential: CredentialInput,
input: { tenantId: string; appId?: string },
): Promise<AssignmentView[]>mine
Returns the caller's launcher: the apps they may open now and the apps they may request.
Used inApplication catalog
- Permission: None beyond a person's own signed-in session of the tenant.
- Audited as: Not audited; it only reads.
- Errors:
ACCESS_DENIED(403) for API keys, role sessions, session tokens, delegated sessions, or a session of another tenant.
Only enabled apps are listed. An app the person has carries via (everyone, direct or group); an app they lack
that names a requestPackageId carries that package id instead, for a request through
packages.request (which needs iam:packages:request on the package). Each
entry has id, key, name, description, category, logoUrl, and lastLaunchedAt once the person opened it.
Apps they have come first, most recently opened first, then by name. In React, useMyApps wraps this and launch.
Prop
Type
An array of MyApp.
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/applications/mine" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.applications.mine(
credential: CredentialInput,
input: { tenantId: string },
): Promise<MyApp[]>removeUnused
Removes an app's direct assignments to people who have not opened it for unusedDays.
Used inApplication catalog
- Permission:
iam:applications:assignon the app. - Audited as:
iam:applications:assign, plusapp:unassignfor each removal, with the reasonunused N days. - Errors:
INVALID_INPUTfor anunusedDaysoutside 7 to 3650;NOT_FOUNDwhen the app is not in this tenant.
It removes exactly the assignments usage lists as unused for the same unusedDays. Group assignments
stay. The result is { removed }, the number removed.
await iam.api.applications.removeUnused(credential, { tenantId, appId, unusedDays: 90 });Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/applications/removeUnused" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"appId": "<appId>",
"unusedDays": 1
}'iam.api.applications.removeUnused(
credential: CredentialInput,
input: { tenantId: string; appId: string; unusedDays: number },
): Promise<{ removed: number }>unassign
Removes one assignment of an app.
- Permission:
iam:applications:assignon the assignment's app, as forassign. - Audited as:
iam:applications:assign, plusapp:unassignwithsubjectTypeandsubjectId, recorded on the app. - Errors:
NOT_FOUNDwhen the assignment is not in this tenant;CONFLICT(409) when it changed during the call (try again).
The person, or the group's members, lose the app at once unless they have it another way. The result is
{ removed: true }.
Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/applications/unassign" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"assignmentId": "<assignmentId>"
}'iam.api.applications.unassign(
credential: CredentialInput,
input: { tenantId: string; assignmentId: string },
): Promise<{ removed: boolean }>update
Changes an app's name, URLs, client, visibility, request package, owners, or whether it is enabled.
- Permission:
iam:applications:manageon the app. - Audited as:
iam:applications:manage, plusapp:updatewith the key and the app's state before and after. - Errors:
INVALID_INPUTas forcreate, and whenkeydiffers from the app's;NOT_FOUNDwhen the app is not in this tenant;CONFLICT(409) when another app already governs the newoauthClientId, or when the app governs an OAuth client whoseoauthClientIdthe call changes or clears withoutreleaseClient: true.
Fields you leave out keep their values; null or an empty string clears description, category, logoUrl,
oauthClientId and requestPackageId, and ownerIds replaces the whole list. Clearing oauthClientId makes the app
a link and leaves its former client open to everyone of the organization, so it needs releaseClient: true, as
does pointing the app at another client. enabled: false takes the app off every launcher and refuses launches, and
sign-ins to its client, while keeping its assignments.
Prop
Type
A Application object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/applications/update" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"appId": "<appId>"
}'iam.api.applications.update(
credential: CredentialInput,
input: Partial<ApplicationInput> & {
tenantId: string;
appId: string;
releaseClient?: boolean;
},
): Promise<Application>usage
Reports, per app, how many people have it, how many opened it in 30 days, and the direct assignments nobody uses.
Used inApplication catalog
- Permission:
iam:applications:readon the tenant. - Audited as:
iam:applications:read. - Errors:
INVALID_INPUTfor anunusedDaysoutside 1 to 3650.
Each entry has appId, key, name, people (active people who have the app now), launchedLast30Days (people
whose last launch was within 30 days), and unused: the direct assignments to people made at least unusedDays ago
(90 by default) whose person has not opened the app within that time or ever, each with assignmentId, identityId,
lastLaunchedAt, and name for callers also allowed iam:identities:read on the tenant. Group assignments and
everyone apps are never unused, and disabled apps count no people.
const report = await iam.api.applications.usage(credential, { tenantId, unusedDays: 60 });Prop
Type
An array of ApplicationUsage.
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/applications/usage" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.applications.usage(
credential: CredentialInput,
input: { tenantId: string; unusedDays?: number },
): Promise<ApplicationUsage[]>Better IAM is created by Sean Filimon
Last updated
analysis
Access analysis scans a tenant's configuration for risky or stale access, such as administrators without MFA or dormant accounts that still hold roles.
assertions
Assertions are short-lived signed tokens that tell another service who is calling: the caller's identity, tenant, roles, groups, and whether they used MFA.