Analyzed Aug 17, 2026
caffeinelabs/skills
extension-authorization
Authorization system with role-based access control. Must-have for all apps that manage personal or access-restricted data.
Installation
npx skills add caffeinelabs/skills --skill extension-authorization
Similar popular skills
Related neighbors and high-traction skills in the same topics — useful to compare before installing.
>- IDOR and broken object authorization testing playbook. Use when requests expose object ident…
3.1K installs>- API authorization and BOLA testing playbook. Use when APIs expose object identifiers, nested…
3K installsProvides patterns for unit testing Spring Security with `@PreAuthorize`, `@Secured`, `@RolesAll…
2.8K installsSecurity audits
Partner security reviews for this skill.
Analyzed Aug 17, 2026
Analyzed Aug 17, 2026
0 alerts
Also in this package
Other skills from caffeinelabs/skills · top by installs.
npx skills add caffeinelabs/skills
More details
Agent compatibility
Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.
Repository health
main
Skill metadata
Parsed from SKILL.md frontmatter.
Package contents
Files included with this skill beyond the listing page.
-
skill md
SKILL.md15,836 B -
docs
SUMMARY.md151 B
History
- First seen on skills.sh
- First recorded snapshot · 18,700 installs
SKILL.md
Authorization
Authorization extendsion for Caffeine AI.
Overview
This skill adds an authentication and authorization system with role-based access control using the mixin pattern. The MixinAuthorization mixin provides standard authorization endpoints automatically.
Backend
Authentication system with role-based access control.
There is a prefabricated library mo:caffeineai-authorization/access-control.mo. It provides core authentication with role-based access control.
Module API
```mo:caffeineai-authorization/access-control.mo module { public type UserRole = { #admin; #user; #guest; };
public type AccessControlState = { / internal state / };
public func initState() : AccessControlState; public func getUserRole(state : AccessControlState, caller : Principal) : UserRole; public func assignRole(state : AccessControlState, caller : Principal, user : Principal, role : UserRole); public func isAdmin(state : AccessControlState, caller : Principal) : Bool; public func hasPermission(state : AccessControlState, caller : Principal, requiredRole : UserRole) : Bool; };
Initialization is handled internally by `MixinAuthorization` -- do not call `initialize` directly. The first authenticated user to log in automatically becomes admin; no token or secret is required.
IMPORTANT: The `include MixinAuthorization(accessControlState)` line MUST be placed in `main.mo`, not in a custom mixin file.
## Opting out / custom authentication
If the user asks to replace this authorization system with custom authentication (for example username/password auth, no Internet Identity, or no `MixinAuthorization`), remove the `caffeineai-authorization` mops package from the project. This is the only supported way to remove the `include-authorization` lint requirement, because that lint rule is shipped by the package. Do not add suppression comments or leave the package installed while omitting `include MixinAuthorization(accessControlState)`.
When removing the package, also remove all `mo:caffeineai-authorization/*` imports, the `accessControlState` initialization, `include MixinAuthorization(accessControlState)`, and any `AccessControl` guard calls that belonged to this component. Replace them with the custom authentication and authorization checks requested by the user.
## Setup in main.mo
```motoko filepath=src/backend/main.mo
import Map "mo:core/Map";
import Principal "mo:core/Principal";
import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
import Types "types";
import ProfileMixin "mixins/Profile";
actor {
let accessControlState : AccessControl.AccessControlState;
include MixinAuthorization(accessControlState, null);
let userProfiles : Map.Map<Principal, Types.UserProfile>;
include ProfileMixin(accessControlState, userProfiles);
};
The migration chain head:
```motoko filepath=src/backend/migrations/00000000_000000.mo import Map "mo:core/Map"; import AccessControl "mo:caffeineai-authorization/access-control";
module { type UserProfile = { name : Text; };
type NewActor = { accessControlState : AccessControl.AccessControlState; userProfiles : Map.Map<Principal, UserProfile>; };
public func migration(_old : {}) : NewActor { { accessControlState = AccessControl.initState(); userProfiles = Map.empty<Principal, UserProfile>(); }; }; };
## Type Definitions in types.mo
```motoko filepath=src/backend/types.mo
module {
public type UserProfile = {
name : Text;
};
};
Custom Mixin Example (mixins/Profile.mo)
The frontend requires getCallerUserProfile, saveCallerUserProfile, and getUserProfile. Pass accessControlState to your mixin so it can check permissions.
```motoko filepath=src/backend/mixins/Profile.mo import Map "mo:core/Map"; import Principal "mo:core/Principal"; import Runtime "mo:core/Runtime"; import AccessControl "mo:caffeineai-authorization/access-control"; import Types "../types";
mixin ( accessControlState : AccessControl.AccessControlState, userProfiles : Map.Map<Principal, Types.UserProfile>, ) { public query ({ caller }) func getCallerUserProfile() : async ?Types.UserProfile { if (not AccessControl.hasPermission(accessControlState, caller, #user)) { Runtime.trap("Unauthorized"); }; userProfiles.get(caller); };
public shared ({ caller }) func saveCallerUserProfile(profile : Types.UserProfile) : async () { if (not AccessControl.hasPermission(accessControlState, caller, #user)) { Runtime.trap("Unauthorized"); }; userProfiles.add(caller, profile); };
public query ({ caller }) func getUserProfile(user : Principal) : async ?Types.UserProfile { if (caller != user and not AccessControl.isAdmin(accessControlState, caller)) { Runtime.trap("Unauthorized: Can only view your own profile"); }; userProfiles.get(user); }; };
## Guard Patterns
Apply the appropriate guard to every public function:
// Admin-only: if (not AccessControl.hasPermission(accessControlState, caller, #admin)) { Runtime.trap("Unauthorized: Only admins can perform this action"); };
// Users only: if (not AccessControl.hasPermission(accessControlState, caller, #user)) { Runtime.trap("Unauthorized: Only users can perform this action"); };
// Any user including guests: No check needed
## Design Guidelines
- Anonymous principals are treated as guests.
- `assignRole` includes an admin-only guard internally.
- Use `shared({ caller })` for authenticated endpoints that modify data.
- Use `query({ caller })` for authenticated endpoints that fetch data.
- Handle ownership verification where needed.
- Use `Runtime.trap` for authorization failures.
## Email Attributes
`MixinAuthorization` can capture the user's verified Internet Identity attributes (name and email) at sign-in. Pass a callback as the second argument instead of `null`; it runs once per sign-in, after the attribute bundle has been verified.
Do NOT use the `mo:identity-attributes` mixin directly -- always go through `MixinAuthorization`. The callback receives the caller principal and the verified attributes:
{ name : ?Text; // verified display name, when present email : ?Text; // always the verified address -- sourced from II's verified_email, never the unverified email key sso : ?Text; // SSO domain when the identity came from SSO, otherwise null }
The field is named `email`, but it only ever holds II's `verified_email` value -- the unverified `email` key is never read. Use `attrs.email` in the callback (there is no `attrs.verified_email` field).
Which attributes arrive depends on the sign-in variant the frontend used (see the `extension-authorization`-compatible `login()` options in the `extension-core-infrastructure` skill):
- `login()` (plain Internet Identity): `email` is present when the user has a verified email in II; `sso` is null.
- `login({ provider: 'google' })`: verified `name` and `email` from the user's Google account; `sso` is null.
- `login({ provider: 'microsoft' })`: verified `name` and `email` from the user's Microsoft account; `sso` is null.
- `login({ ssoDomain: 'acme.com' })`: verified `name` and `email` from the company's identity provider; `sso` holds the domain (e.g. `"acme.com"`). Use `attrs.sso` to gate features to company members or auto-assign roles by domain.
Store them in your own state and expose a getter to read them back:
<!-- motoko-check:skip -->
import Map "mo:core/Map"; import Principal "mo:core/Principal"; import AccessControl "mo:caffeineai-authorization/access-control"; import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
actor { let accessControlState : AccessControl.AccessControlState;
let emails : Map.Map<Principal, Text>;
include MixinAuthorization( accessControlState, ?(func(caller : Principal, attrs : { name : ?Text; email : ?Text; sso : ?Text }) { switch (attrs.email) { case (?email) { emails.add(caller, email) }; case null {}; }; }), );
public query ({ caller }) func getCallerEmail() : async ?Text { emails.get(caller); }; };
The `trusted_attribute_signers` and `frontend_origins` canister environment variables required for attribute verification are configured automatically by the Caffeine platform -- you do not set them.
### Fetching the Email on the Frontend
After sign-in, query the getter like any other authenticated actor method:
const { data: callerEmail } = useQuery<string | null>({ queryKey: ['callerEmail'], queryFn: () => actor.getCallerEmail(), enabled: !!actor && isAuthenticated, });
# Frontend
Authentication system with role-based access control.
## Sign-In Options
Sign-in goes through the `useInternetIdentity` hook from `@caffeineai/core-infrastructure` (see the `extension-core-infrastructure` skill for the full API and UI pattern). Default to plain `login()` unless Google, Microsoft, or company/workspace SSO sign-in was requested.
Rules:
- When Google, Microsoft, or SSO sign-in **is** requested, the sign-in screen must show the requested direct option (Google button, Microsoft button, and/or SSO domain input) **and** keep a plain `login()` "Sign in with Internet Identity" button as a fallback — users without a Google or Microsoft account or a registered company domain must still be able to sign in.
- Never remove the Internet Identity option from an existing app that already has users. The principal is derived from the user's Internet Identity: a user whose II identity is Google- or Microsoft-backed gets the **same principal** through either button, but a user who signed up with a passkey-based II identity gets a **different principal** if they authenticate with a Google or Microsoft account that is not linked to that identity — their data would appear missing. Keeping both buttons lets every existing user sign in the way they always have.
## User Profile Setup
When using Internet Identity, the user gets a principal id only after login. Anonymous principals are treated as guests. The principal id is not human-readable -- ask the user for their name the first time they log in with a new principal.
Backend API for profiles:
- `getCallerUserProfile(): Promise<UserProfile | null>` -- returns `null` if no profile exists
- `saveCallerUserProfile(profile: UserProfile): Promise<void>` -- saves name and profile data
- `getUserProfile(user: Principal): Promise<UserProfile | null>` -- fetch another user's profile
Rules:
- On login, if the user already has a profile, do not ask for the name again
- Display the user's profile name instead of the principal id
- Make sure the user must be logged in before seeing any application data
- When logging out, clear all cached application data including the cached user profile
### Preventing Profile Setup Modal Flash
export function useGetCallerUserProfile() { const { actor, isFetching: actorFetching } = useActor();
const query = useQuery<UserProfile | null>({ queryKey: ['currentUserProfile'], queryFn: async () => { if (!actor) throw new Error('Actor not available'); return actor.getCallerUserProfile(); }, enabled: !!actor && !actorFetching, retry: false, });
return { ...query, isLoading: actorFetching || query.isLoading, isFetched: !!actor && query.isFetched, }; }
Then in your component:
const showProfileSetup = isAuthenticated && !profileLoading && isFetched && userProfile === null;
## Auth State Lifecycle
The `useInternetIdentity` hook exposes two kinds of state — use the right one:
| Scenario | `loginStatus` | `isAuthenticated` |
|---|---|---|
| Page load, no stored session | `"idle"` | `false` |
| Page load, restoring stored session | `"initializing"` | `false` → `true` |
| Stored session restored after reload | `"idle"` | `true` |
| Interactive login in progress (popup open) | `"logging-in"` | `false` |
| Interactive login just completed | `"success"` | `true` |
| Login popup failed / cancelled | `"loginError"` | `false` |
**IMPORTANT:** `isLoginSuccess` (`loginStatus === "success"`) is only `true` after an interactive login via the popup. It is **NOT** `true` when a stored identity is restored on page reload. Never use `isLoginSuccess` to gate authenticated vs. unauthenticated UI — always use `isAuthenticated`.
Key states for the login button:
- `isInitializing` — `AuthClient` is loading from IndexedDB; disable the button to prevent clicks before the client is ready.
- `isLoggingIn` — the II popup is open; disable the button to prevent duplicate popups.
## Login Component
import { useInternetIdentity } from '@caffeineai/core-infrastructure'; import { useQueryClient } from '@tanstack/react-query';
export default function LoginButton() { const { login, clear, isAuthenticated, isInitializing, isLoggingIn } = useInternetIdentity(); const queryClient = useQueryClient();
const handleAuth = () => { if (isAuthenticated) { clear(); queryClient.clear(); } else { login(); } };
return ( <button onClick={handleAuth} disabled={isInitializing || isLoggingIn} className={px-6 py-2 rounded-full transition-colors font-medium ${ isAuthenticated ? 'bg-gray-200 hover:bg-gray-300 text-gray-800' : 'bg-blue-600 hover:bg-blue-700 text-white' } disabled:opacity-50} > {isInitializing ? 'Loading...' : isAuthenticated ? 'Logout' : 'Login'} </button> ); }
The `login()` and `clear()` functions are fire-and-forget (they don't return promises that track the full flow). The hook's `isLoggingIn` / `isInitializing` states track the async lifecycle — do **not** wrap them in local `useState` / `isPending` logic.
When the app calls for Google or Microsoft sign-in or company/workspace SSO, use the same hook with options: `login({ provider: 'google' })`, `login({ provider: 'microsoft' })`, or `login({ ssoDomain: 'acme.com' })`. All variants produce the same identity and session behavior; see the `extension-core-infrastructure` skill for the full sign-in UI pattern.
Gate authenticated UI on `isAuthenticated` (covers both fresh login and restored sessions on page reload):
{isAuthenticated ? ( <AuthenticatedApp /> ) : ( <LoginScreen /> )}
## Comparing Current User with Data Author
import { useInternetIdentity } from '@caffeineai/core-infrastructure'; import type { Principal } from '@icp-sdk/core/principal';
const { identity } = useInternetIdentity();
const isAuthor = (authorPrincipal: Principal): boolean => { if (!identity) return false; return authorPrincipal.toString() === identity.getPrincipal().toString(); };
## Access Control UI
For admin-only or personal applications, show an AccessDeniedScreen component when unauthorized users try to access the application.
## Error Handling
Handle authorization errors from backend `Debug.trap` calls gracefully in the UI with appropriate error messages shown to the user.
Note: The initialization of the first admin is done automatically in `@caffeineai/core-infrastructure`. The first authenticated user to log in becomes admin; no token or secret is needed.