Appearance
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 asposts:read,billing.exportorreports-admin. - Standard OpenID Connect scopes (
openid,profile,email,offline_access,roles,phoneandaddress) can't be used, and names starting withmicroauthare 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, orhttponlocalhost,127.0.0.1or[::1]for development.- A lowercase host, no default port, and no query or fragment.
- No trailing slash on its own:
https://api.example.com, nothttps://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,profileandemailare always available.rolesaddsrolesandpermissionsclaims to the ID token and userinfo. Only your own applications can get it.offline_accessis granted only to apps allowed to use refresh tokens.- Any other scope is a permission. The request must name the server with the
resourceparameter, and that server must expose the permission. Otherwise the request fails withinvalid_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 buttonauth.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.