loginos

How do I manage user roles across multiple apps?

loginos teamUpdated 7 min read

Short answer

Keep roles in one central place, usually your identity provider, and have every app read them from there instead of keeping its own copy. Name permissions per app, like billing:invoices:refund, deliver them in tokens at sign-in, re-check them through an API, and offboard people by blocking their account once. In loginos, one platform’s roles apply on all of its domains.

Your product has grown into several apps: the main app, an admin console, a help center, maybe a shop on its own domain. Each one started with its own is_admin column. Now giving a support agent access means editing three databases, and when someone leaves, one of them gets forgotten.

The fix is a decision about architecture. One place decides who has which role, every app reads from there, and each app checks permissions in its own code.

Should roles live in one place or in each app?

There are three ways to split the work between a central store and your apps:

ModelWhere roles liveStrengthWeakness
Per appEach app’s own databaseEach team changes roles without asking anyoneRoles drift apart, and offboarding misses an app
CentralYour identity provider or an authorization serviceOne place to grant, revoke and reviewEvery app depends on one shared model
HybridCentral for global roles, each app for rights on single objectsEach kind of right lives where it is checkedTwo places to look when you debug access

Most teams end up with the hybrid. Global facts belong in the central store: who is staff, who is an editor, who may refund orders. Facts about single objects stay in the app that owns them: who owns project 7, who belongs to workspace 12. Those change all day and matter to one app only.

A central store also answers the audit question “who can refund orders?” in one query. With per-app roles, you ask every database and hope they agree.

How should you name roles and permissions?

Role-based access control (RBAC) gives people roles and gives roles permissions. NIST’s model of it became the ANSI/INCITS 359 standard in 2004. In RBAC, your code checks permissions, the small and stable part, and roles stay with the people who manage access.

With several apps, put the app’s name in each permission key, in an app:resource:action pattern:

RolePermissions
member (the default)blog:comments:write, shop:orders:create
editorblog:posts:write, blog:posts:publish
support-agentadmin:users:read, shop:orders:read, shop:orders:refund

The prefix shows which app owns a permission, and two apps can both have posts without a clash. A role can combine permissions from several apps, because roles describe jobs, and jobs cross app boundaries.

Watch for role explosion, which the OWASP Authorization Cheat Sheet describes as defining too many roles. If you find yourself creating editor-workspace-12, that is per-tenant data. It belongs in the app, not in the global role list.

How do roles reach each app?

Apps learn about roles in four ways, and most setups combine two or three of them:

WayHow freshWhat it costs
Claims in the token at sign-inAs of the sign-inNothing: the roles arrive with the person
The OpenID Connect userinfo endpointCurrentA request that needs a valid access token
A management API, called by your serverCurrentA server-to-server request, within rate limits
Pushed changes: webhooks or SCIMUsually secondsAn endpoint you secure, retry and keep idempotent

Token claims are the cheapest and go stale first, because a token shows the roles someone had when it was issued. The JWT roles answer covers lifetimes and verification. A pull from userinfo or a management API is current, but it costs a request, so cache the result for a few minutes.

Pushed changes are the fastest when your provider supports them. A webhook calls your endpoint when a role changes. SCIM (System for Cross-domain Identity Management, RFC 7644) is a protocol that creates and updates users and groups in other systems, mostly in enterprise setups.

A pattern that works for most products:

  1. Read roles from the token at sign-in and keep them in the app’s own session.
  2. Re-read them every few minutes, and before sensitive actions like refunds or role changes.
  3. End the session when the re-read says the account is blocked.

What role should a new user get?

New accounts should get one minimal default role, like member or viewer, automatically. Never make a privileged role the default: one wrong default hands admin rights to everyone who signs up.

Grant staff roles by hand, to a named person, ideally after a second person approves. Inside your product, the person who creates a workspace or team usually becomes its owner. That ownership is a per-tenant fact your app records, not a global role.

How do you keep access small and auditable?

Least privilege means each person holds only the permissions their job needs. Four habits keep it that way:

  • Split read and write. shop:orders:read and shop:orders:refund are separate permissions, so support can look at an order without refunding it.
  • Review regularly. Every quarter, list who holds each powerful role, and remove what nobody can explain.
  • Log every change. Record who changed whose roles, when, and from what to what. If your provider doesn’t keep that history, write it from the code that calls its API.
  • Offboard in one place. Block the account at the identity provider, so sign-in stops in every app at once, then end the sessions your apps hold. Removing roles alone isn’t enough: the person can still sign in, and every app that grants something to any signed-in account still lets them in.

How loginos does it

In loginos, your product is a platform, and one platform can run on any number of domains with one pool of members. Roles and permissions belong to the platform, so they apply on all of its domains. Put your main app, admin console and shop on one platform, even on app.example.com, admin.example.net and shop.example.org, and all three read the same roles. Domains explains how to add them.

Jane’s roles

Tap to try your own mix.

{  "sub": "9b1f6c2e-…",  "preferred_username": "jane",  "roles": [    "member"  ],  "permissions": [    "comments:write",    "posts:read"  ]}
1/3Everyone who joins gets the default role, member.
  • Define permissions and roles in the console’s Roles tab. Permission keys allow lowercase letters, digits and _ . : -, so shop:orders:refund works. Role keys like support-agent group them.
  • Default roles go to everyone who joins the platform.
  • Delivery: the ID token, the token endpoint’s user object and /oauth/userinfo always carry roles and permissions. For current values, call userinfo with the access token, or GET /api/v1/users/{sub} from your server.
  • Changes: use the console’s Users tab or the Management API. GET /api/v1/roles lists your roles with their permissions.
  • Offboarding is blocking: sign-in stops on all your domains, existing tokens stop working at once, and the roles are kept for when you unblock.
curl
# Make Jane a support agent on every domain of the platform. The list replaces her roles.
curl -X PUT https://loginos.io/api/v1/users/SUB/roles \
  -u "$LOGINOS_APP_KEY:$LOGINOS_APP_SECRET" \
  -H "content-type: application/json" \
  -d '{"roles": ["member", "support-agent"]}'

# When she leaves: no more sign-ins on any of your domains.
curl -X POST https://loginos.io/api/v1/users/SUB/block -u "$LOGINOS_APP_KEY:$LOGINOS_APP_SECRET"

Some parts stay with you. Roles are per platform, so two products on two platforms have separate roles, even though people use the same loginos account on both. A platform has no organizations or tenants, so roles per customer workspace live in your database. loginos doesn’t push changes either: there are no webhooks and no SCIM, so your apps re-read roles from the API. If you need roles per organization, or provisioning from your customers’ directories, compare providers with built-in roles. The roles guide has the details.

Frequently asked questions

Should each app define its own roles?

Each app should define its own permissions, prefixed with its name, because the app knows which actions it has. Roles work better in one central place, because they describe people’s jobs, and jobs span apps. A support agent needs to read users in the admin console and refund orders in the shop, under one role.

How do I give users different roles in different workspaces?

Store a membership per workspace in your app: the user, the workspace and the role. Global roles in a token can’t say “admin here, viewer there” without one role per workspace, which doesn’t scale. Some providers have organizations with their own roles. loginos doesn’t, so with loginos that table lives in your database, keyed by sub.

What is the difference between RBAC, ABAC and ReBAC?

RBAC (role-based access control) decides by the roles a person holds. ABAC (attribute-based access control) decides by attributes of the person, the resource and the request, like a department or the time of day. ReBAC (relationship-based access control) decides by relationships, like “Jane owns this document”. Many products use RBAC for global rights and ReBAC for single objects.

How fast do role changes reach my apps?

As fast as the slowest channel you rely on. Claims in tokens change at the next sign-in or token refresh. A userinfo or management API request shows the change at once. Webhooks, where a provider has them, usually arrive within seconds. Blocking should take effect everywhere at once, so check for it on every re-read.

How do I remove someone’s access to all apps at once?

Block or disable the account at your identity provider, so no app can sign them in again. Then end the sessions your apps already hold, through the same re-read that refreshes roles. With loginos, POST /api/v1/users/{sub}/block stops sign-in on every domain of the platform.

One account for every domain you run

loginos is an OpenID Connect provider with passkeys, a user pool shared by all of your domains, roles in every token, and account linking for the users you already have. Add your platform and its domains in a few minutes.

Keep reading

All answers