Skip to content

Roles and permissions ​

Permissions are what your code checks, such as posts:read or invoices:write. Roles bundle permissions, and you give roles to people. Your APIs and MCP servers each expose the permissions that make sense for them, and those permissions are the OAuth scopes apps ask for.

All of it lives under Roles and permissions and APIs and MCP servers in the dashboard.

Permissions ​

A permission has a name and a description. The description is what people read on the consent screen, so write it in plain words, such as "Create and edit your posts". A permission without a description shows its name there instead, such as posts:write. While apps people connect are on, the dashboard marks the permissions that still need one.

Names follow a strict pattern so they work as OAuth scopes everywhere:

  • 2 to 64 characters: lowercase letters and digits, starting with a letter.
  • Words separated by a single :, ., _ or -, such as posts:read, billing.export or reports-admin.
  • Standard OpenID Connect scopes (openid, profile, email, offline_access, roles, phone and address) can't be used, and names starting with microauth are reserved.

Mark a permission sensitive when it allows something people should think twice about, such as deleting data or spending money. The consent screen highlights it.

A service can have up to 200 permissions.

Roles ​

A role is a named set of permissions, such as Editor with posts:read and posts:write. A person's permissions are everything their roles grant.

  • Give roles to people on their page under Users, or pick them when you invite someone.
  • Mark roles as default to give them to everyone who signs up, including through external sign in.
  • A service can have up to 50 roles.

APIs and MCP servers ​

Register each server that accepts access tokens under APIs and MCP servers. A server is identified by its URL, such as https://api.example.com or https://app.example.com/mcp, and you choose which permissions it exposes.

The URL is written in one canonical form, and your server must use exactly the same string, as MICROAUTH_RESOURCE in the SDK:

  • https, or http on localhost, 127.0.0.1 or [::1] for development.
  • A lowercase host, no default port, and no query or fragment.
  • No trailing slash on its own: https://api.example.com, not https://api.example.com/.

An access token names one server in its aud claim, so a token issued for one server is refused at another, even on the same sign in service.

How scopes are granted ​

When an app asks for scopes, MicroAuth decides what it gets:

  • openid, profile and email are always available.
  • roles adds roles and permissions claims to the ID token and userinfo. Only your own applications can get it.
  • offline_access is granted only to apps allowed to use refresh tokens.
  • Any other scope is a permission. The request must name the server with the resource parameter, and that server must expose the permission. Otherwise the request fails with invalid_scope.
  • Permissions the person doesn't hold are left out of the token without an error, so an app can ask for everything it might use.
  • Apps people connect also need the person's consent for each scope.

Grants are worked out again at every refresh. When you take a role away, the permissions it gave disappear from the next access token, at most one access token lifetime later.

Check them in FastAPI ​

In a web app with UserAuth, roles and permissions come with the user:

python
from fastapi import Depends, FastAPI
from microauth_fastapi import User, UserAuth

app = FastAPI()
auth = UserAuth(app)


@app.delete("/posts/{post_id}")
async def delete_post(post_id: str, user: User = Depends(auth.require(permissions="posts:delete"))):
    ...


@app.get("/admin")
async def admin(user: User = Depends(auth.require(roles="admin", redirect=True))):
    ...


@app.get("/posts/{post_id}")
async def read_post(post_id: str, user: User = Depends(auth.user)):
    if user.has_permission("posts:write"):
        ...  # show the edit button

auth.require takes one name or a list, and the user needs every one. In an API or MCP server, check the scopes of the access token instead, with TokenAuth or McpAuth:

python
from fastapi import Depends, FastAPI
from microauth_fastapi import TokenAuth, TokenClaims

app = FastAPI()
api = TokenAuth(app)


@app.get("/posts")
async def posts(token: TokenClaims = Depends(api.require("posts:read"))):
    ...

Permissions say what kind of action someone may take, not which records they may touch. Check that a post belongs to the person in your own code.

MicroAuth is a product of Zyref, LLC.