Skip to content
Back to skills

Authorization Rbac

ASecurity

RBAC and ABAC in the application layer: defining roles and claims, mapping the authenticated identity to roles, enforcing roles in middleware and UI. The critical seam: row-level data scoping hands off to data-platform RLS via auth.uid(). Authentication proves identity; authorization controls access.

  • 7 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 23, 2026
ai-agentsgosqlreactnextjsexpressapidatabasesecurity

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 23, 2026

npx -y skills add mcorbett51090/RavenClaude --skill authorization-rbac --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Authorization Rbac?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Authorization Rbac
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mcorbett51090-authorization-rbac/badge)](https://www.skillsdirectory.com/skills/mcorbett51090-authorization-rbac)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: authorization-rbac
description: "RBAC and ABAC in the application layer: defining roles and claims, mapping the authenticated identity to roles, enforcing roles in middleware and UI. The critical seam: row-level data scoping hands off to data-platform RLS via auth.uid(). Authentication proves identity; authorization controls access."
---

# Skill: authorization-rbac

> **Invoked by:** any agent designing access-control rules for a web app or API; `ravenclaude-core/security-reviewer` for any role-enforcement change.
>
> **When to invoke:** defining who can access what (roles, permissions); mapping Google-authenticated users to application roles; implementing admin-only sections; designing the handoff between this plugin's identity layer and data-platform's RLS layer.
>
> **Output:** role model documented + roles stored/retrieved + enforcement in middleware + UI conditional rendering + data-row seam to data-platform RLS documented.

---

## Boundary: authentication ≠ authorization ≠ data row access

Three distinct layers — keep them separate:

| Layer | What it answers | Owned by |
|---|---|---|
| **Authentication** | "Who is this person?" — verifies Google identity, establishes `auth.uid()` | This plugin (`auth-identity`) |
| **Application authorization** | "What is this person allowed to do?" — roles, permissions, feature flags | This skill |
| **Data authorization** | "Which rows can this person read?" — row-level scoping by tenant/org | `data-platform` plugin (`rls-policy-authoring` skill) |

A logged-in user is not automatically entitled to every row in the database. A user with the `admin` role in the app is not automatically entitled to another tenant's data. These are separate controls enforced at separate layers.

The best-practice `authenticate-the-person-authorize-the-data-separately.md` is an absolute rule in this plugin.

---

## RBAC vs ABAC

| Model | Structure | When to use |
|---|---|---|
| **RBAC** (Role-Based Access Control) | User → Role → Permissions. Roles are pre-defined (admin, editor, viewer). | Most web apps. Simple, auditable, easy to reason about. |
| **ABAC** (Attribute-Based Access Control) | Access based on attributes of the user, resource, and environment. More flexible, more complex. | When roles are insufficient — e.g., "can edit documents they own" or "can access based on department + resource tag". |
| **ReBAC** (Relationship-Based) | Access based on graph relationships (user owns resource, is member of group). | Very fine-grained access (Google Zanzibar pattern). Complex; usually overkill for SMB apps. |

**Default to RBAC.** Add ABAC rules only when a concrete access requirement cannot be expressed as a role.

---

## Storing roles

### Option A — Supabase custom claims / user metadata (recommended)

Store roles in Supabase Auth's `user_metadata` or `app_metadata`. `app_metadata` is server-editable only (not alterable by the user):

```sql
-- Set role server-side (admin action only — never let the user set their own app_metadata)
-- Supabase Admin API or a server-side function:
UPDATE auth.users
SET raw_app_meta_data = raw_app_meta_data || '{"role": "admin"}'::jsonb
WHERE id = '<user_id>';
```

Read in a Server Component or API route:

```ts
const { data: { user } } = await supabase.auth.getUser();
const role = user?.app_metadata?.role ?? "viewer"; // default to lowest privilege
```

[unverified — confirm `app_metadata` write behavior in current Supabase Auth API]

### Option B — Roles table in the application database

For complex role models (multiple roles per user, org-scoped roles):

```sql
-- Roles table — created once per environment
CREATE TABLE user_roles (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id     uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  role        text NOT NULL CHECK (role IN ('admin', 'editor', 'viewer')),
  created_at  timestamptz DEFAULT now(),
  UNIQUE(user_id, role)
);

-- RLS: users can read their own roles; only admins can write
ALTER TABLE user_roles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "users can read own roles" ON user_roles
  FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "admins can manage roles" ON user_roles
  USING (EXISTS (
    SELECT 1 FROM user_roles r2
    WHERE r2.user_id = auth.uid() AND r2.role = 'admin'
  ));
```

### Option C — Roles in the JWT (custom claims)

Embed roles in the Supabase JWT via a custom claims function [unverified — Supabase custom JWT claims feature; verify in current docs]:

```sql
-- Supabase SQL editor: custom claim function
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
RETURNS jsonb AS $$
DECLARE
  claims jsonb;
  user_role text;
BEGIN
  SELECT role INTO user_role FROM public.user_roles WHERE user_id = (event->>'userId')::uuid LIMIT 1;
  claims := event->'claims';
  claims := jsonb_set(claims, '{app_role}', to_jsonb(coalesce(user_role, 'viewer')));
  RETURN jsonb_set(event, '{claims}', claims);
END;
$$ LANGUAGE plpgsql;
```

This embeds the role in the JWT so API routes can read it without a database round-trip. Route to `ravenclaude-core/security-reviewer` — custom claim functions are security-critical.

---

## Enforcing roles in Next.js middleware

```ts
// middleware.ts — role-based route protection
// Assumes role is in user.app_metadata.role (Option A)
import { createServerClient } from "@supabase/ssr";
import { NextResponse } from "next/server";

const ADMIN_PATHS = ["/admin", "/admin/:path*"];
const EDITOR_PATHS = ["/edit/:path*"];

export async function middleware(request) {
  // ... (session setup from protect-spa-and-api skill)
  const { data: { user } } = await supabase.auth.getUser();
  const role = user?.app_metadata?.role ?? "viewer";

  const path = request.nextUrl.pathname;

  if (ADMIN_PATHS.some((p) => path.startsWith(p.replace("/:path*", ""))) && role !== "admin") {
    return NextResponse.redirect(new URL("/unauthorized", request.url));
  }

  if (EDITOR_PATHS.some((p) => path.startsWith(p.replace("/:path*", ""))) && !["admin", "editor"].includes(role)) {
    return NextResponse.redirect(new URL("/unauthorized", request.url));
  }

  return NextResponse.next();
}
```

---

## Enforcing roles in API routes

Never rely on UI-only gating — API routes must check roles independently:

```ts
// app/api/admin/route.ts
export async function DELETE(request: Request) {
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return new Response("Unauthorized", { status: 401 });

  const role = user.app_metadata?.role;
  if (role !== "admin") return new Response("Forbidden", { status: 403 });

  // proceed with admin action
}
```

---

## Conditional rendering in the UI

```tsx
// components/AdminPanel.tsx
"use client";
import { useEffect, useState } from "react";
import { createClient } from "@/lib/supabase/client";

export function AdminPanel() {
  const [isAdmin, setIsAdmin] = useState(false);
  const supabase = createClient();

  useEffect(() => {
    supabase.auth.getUser().then(({ data: { user } }) => {
      setIsAdmin(user?.app_metadata?.role === "admin");
    });
  }, [supabase]);

  if (!isAdmin) return null; // UI gating only — API must enforce independently
  return <div>Admin controls</div>;
}
```

**UI gating alone is not a security control.** API routes must enforce roles regardless of what the UI renders.

---

## The identity → RLS seam (data authorization handoff)

Once the user's identity and application role are established, data-row scoping is the `data-platform` plugin's responsibility:

1. `auth.uid()` from the Supabase JWT is available in Postgres RLS policies automatically — the Supabase client passes the JWT in every query.
2. The `data-platform/rls-policy-authoring` skill defines policies like:
   ```sql
   -- Users can only read rows belonging to their tenant
   CREATE POLICY "tenant_isolation" ON orders
     FOR SELECT USING (tenant_id = (
       SELECT tenant_id FROM tenant_memberships WHERE user_id = auth.uid()
     ));
   ```
3. The `auth-identity` plugin's job ends at establishing `auth.uid()`. The RLS policy determines which rows that user can read.

**Never duplicate RLS logic in application code** as the load-bearing control. App-code row filters are acceptable as redundant defense-in-depth layers, but the authoritative control is the database RLS policy. See `data-platform/best-practices/enforce-tenant-isolation-closest-to-data.md`.

---

## Anti-patterns this skill flags

- Checking roles only in UI components and not in API routes — API routes must enforce independently
- Storing roles in `user_metadata` (user-editable) instead of `app_metadata` (server-only) [unverified — confirm Supabase metadata editability]
- Admin-role check that accepts any truthy `role` value instead of explicitly checking `role === 'admin'`
- Using UI-rendered role gating as the sole data-access control — RLS must be the load-bearing layer
- No default role — always assign the least-privilege role (`viewer`) if none is set
- Roles stored in localStorage or client-side state — read from server-side session only
- Hard-coding `user.id` in a URL parameter to determine which data to show — derive tenant from the authenticated session, not the URL

---

## See also

- Skill: [`../protect-spa-and-api/SKILL.md`](../protect-spa-and-api/SKILL.md) — route guards that this skill extends with role checks
- Skill: [`../gate-the-dashboard/SKILL.md`](../gate-the-dashboard/SKILL.md) — dashboard-specific role gate + RLS handoff
- Best-practice: [`../../best-practices/authenticate-the-person-authorize-the-data-separately.md`](../../best-practices/authenticate-the-person-authorize-the-data-separately.md)
- data-platform: [`../../../data-platform/skills/rls-policy-authoring/SKILL.md`](../../../data-platform/skills/rls-policy-authoring/SKILL.md) — the data-row authorization layer that follows this skill
- Security escalation: [`../../../ravenclaude-core/agents/security-reviewer.md`](../../../ravenclaude-core/agents/security-reviewer.md)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…