Yeti - The Performance Platform for Agent-Driven Development. Schema-driven APIs, real-time streaming, and vector search. From prompt to production.
A complete authentication and authorization demo. Three auth methods, role-based field masking, and a live React UI -- all in one yeti application.
Demo-authentication shows how yeti's built-in auth system handles Basic auth, JWT tokens, and OAuth providers side-by-side. Admins see every field on an employee record. Viewers see the same endpoint but sensitive fields -- salary, SSN, home address, personal email -- are stripped server-side before the response leaves the wire. No application code required. The schema defines the data, the roles define the permissions, and yeti enforces both automatically.
Authentication tutorials usually demonstrate one method in isolation. Real applications need all three: service-to-service calls use Basic auth, SPAs use JWT, and end users expect "Sign in with Google." Building that means configuring three separate middleware layers, writing token validation logic, and hand-rolling field-level access control.
This demo collapses all of that into yeti's declarative auth system:
- Three auth methods, zero application code -- Basic, JWT, and OAuth are declared in
Cargo.tomlunder[package.metadata.auth]and enforced by the yeti-auth extension. No middleware to write, no token parsing, no session management. - Attribute-level permissions -- roles define which table fields are visible. The
viewerrole cannot readsalary,ssn,homeAddress, orpersonalEmail. The server strips these fields before serialization, not after. - OAuth role mapping -- provider-based rules assign roles automatically. Google users get
admin, GitHub users getviewer. No user provisioning step required. - Seed data on startup --
authLoaderanddataLoaderpopulate users, roles, and employee records on first boot. The demo is functional immediately. - Live React UI -- a single-page app lets you log in with each method, switch between users, and see the RBAC difference in real time.
cd ~/yeti/applications
git clone https://github.com/yetirocks/demo-authentication.gitRestart yeti. The frontend builds automatically on first load via npm run build and is served as a static SPA.
curl -s https://localhost:9996/demo-authentication/Employee/?limit=5 \
-u admin:admin123 | jqResponse (admin -- all fields visible):
[
{
"id": "emp-001",
"name": "Alice Johnson",
"email": "alice@example.com",
"department": "Engineering",
"title": "Senior Developer",
"salary": 145000,
"ssn": "123-45-6789",
"homeAddress": "123 Oak Street, San Francisco, CA",
"personalEmail": "alice.j@personal.com"
}
]curl -s https://localhost:9996/demo-authentication/Employee/?limit=5 \
-u user:user123 | jqResponse (viewer -- sensitive fields stripped):
[
{
"id": "emp-001",
"name": "Alice Johnson",
"email": "alice@example.com",
"department": "Engineering",
"title": "Senior Developer"
}
]The same endpoint, the same data, different fields. The viewer role has attribute_permissions that deny read access to salary, ssn, homeAddress, and personalEmail.
# Obtain a token pair
curl -s -X POST https://localhost:9996/yeti-auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin123"}' | jqResponse:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900
}# Use the access token
curl -s https://localhost:9996/demo-authentication/Employee/?limit=5 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." | jq# Refresh when the access token expires (15 min default)
curl -s -X POST https://localhost:9996/yeti-auth/jwt_refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token": "eyJhbGciOiJIUzI1NiIs..."}' | jqResponse:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...<new>",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...<new>",
"token_type": "Bearer",
"expires_in": 900
}OAuth requires a browser. The flow works like this:
# 1. Redirect the user to the OAuth login endpoint
# (open in browser -- this initiates the provider redirect)
open "https://localhost:9996/yeti-auth/oauth_login?provider=github&redirect_uri=/demo-authentication/"
# 2. After the provider callback, the user lands back at the app
# with an authenticated session cookie
# 3. Check the session
curl -s https://localhost:9996/yeti-auth/oauth_user \
--cookie "yeti_session=<session-cookie>" | jqResponse:
{
"authenticated": true,
"provider": "github",
"user": {
"login": "octocat",
"name": "The Octocat",
"email": "octocat@github.com",
"avatar_url": "https://avatars.githubusercontent.com/u/..."
}
}# 4. Discover available providers and their role mappings
curl -s "https://localhost:9996/yeti-auth/oauth_providers?app_id=demo-authentication" | jqResponse:
{
"providers": [
{ "name": "github" },
{ "name": "google" }
],
"roles": {
"github": "viewer",
"google": "admin"
}
}https://localhost:9996/demo-authentication/
The React frontend provides a visual interface for all three auth methods. Log in with Basic or JWT using the seeded credentials, or click an OAuth provider button. After login, click "GET /Employee" to see the role-based field masking in action.
Browser / curl / Agent
|
+-- Basic auth header ----> yeti-auth (BasicAuthProvider)
+-- Bearer JWT token -----> yeti-auth (JwtAuthProvider)
+-- OAuth redirect -------> yeti-auth (OAuthAuthProvider)
|
v
+--------------------------------------------------+
| yeti-auth extension |
| +---------------+ +------------------+ |
| | Credential | | Role | |
| | validation | | resolution | |
| | (Argon2id) | | (User -> Role) | |
| +---------------+ +------------------+ |
| | | |
| v v |
| +------------------------------------------+ |
| | Attribute-level permission enforcement | |
| | (strip fields denied by role) | |
| +------------------------------------------+ |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| demo-authentication |
| +------------+ +---------+ +--------+ |
| | Employee | | User | | Role | |
| | (data) | | (auth) | | (auth) | |
| +------------+ +---------+ +--------+ |
| |
| React SPA (login UI + RBAC visualization) |
+--------------------------------------------------+
|
v
Yeti (embedded RocksDB, yeti-auth extension)
Auth flow: Request arrives -> yeti-auth inspects Authorization header or session cookie -> validates credentials (Argon2id hash check, JWT signature, or OAuth session lookup) -> resolves user to role -> applies attribute_permissions to strip denied fields -> returns filtered response.
Role resolution: Basic/JWT -> User.roleId -> Role table lookup. OAuth -> config rules match provider name to role ID -> Role table lookup.
HTTP Basic auth using the Authorization: Basic <base64> header. Credentials are validated against the User table with Argon2id password hashing. A 5-minute credential cache avoids repeated hash computations.
# Admin -- full access
curl -u admin:admin123 https://localhost:9996/demo-authentication/Employee/?limit=5
# Viewer -- restricted fields
curl -u user:user123 https://localhost:9996/demo-authentication/Employee/?limit=5Seeded users:
| Username | Password | Role | Access Level |
|---|---|---|---|
admin |
admin123 |
admin | All fields, full CRUD |
user |
user123 |
viewer | Public fields only, read-only |
JSON Web Token auth using the Authorization: Bearer <token> header. Tokens are issued by POST /yeti-auth/login and contain embedded permissions, so authenticated requests skip the database lookup.
| Parameter | Value |
|---|---|
| Algorithm | HS256 |
| Access token TTL | 900 seconds (15 minutes) |
| Refresh token TTL | 604800 seconds (7 days) |
| Secret | ${JWT_SECRET} environment variable |
Token lifecycle:
POST /yeti-auth/loginwithusernameandpassword-- returnsaccess_token+refresh_token- Use
access_tokeninAuthorization: Bearerheader for API calls - When
access_tokenexpires,POST /yeti-auth/jwt_refreshwithrefresh_token-- returns a new token pair DELETE /yeti-auth/loginto invalidate the session
Browser-based OAuth 2.0 with automatic role assignment. Users are redirected to the provider, authenticated, and redirected back with a session cookie. No user pre-provisioning is needed -- role is determined by provider-based rules under [package.metadata.auth] in Cargo.toml.
Configured providers:
| Provider | Role Assigned | Config Required |
|---|---|---|
admin |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
|
| GitHub | viewer |
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET |
OAuth endpoints:
| Endpoint | Method | Description |
|---|---|---|
/yeti-auth/oauth_login |
GET | Initiate OAuth flow (query: provider, redirect_uri) |
/yeti-auth/oauth_callback |
GET | Handle provider redirect (internal) |
/yeti-auth/oauth_user |
GET | Check current OAuth session |
/yeti-auth/oauth_logout |
POST | Clear OAuth session |
/yeti-auth/oauth_refresh |
POST | Refresh provider access token |
/yeti-auth/oauth_providers |
GET | List available providers and role mappings |
OAuth flow:
- Frontend redirects to
/yeti-auth/oauth_login?provider=github&redirect_uri=/demo-authentication/ - yeti-auth redirects to GitHub's authorization page
- User authorizes the application
- GitHub redirects to
/yeti-auth/oauth_callbackwith an authorization code - yeti-auth exchanges the code for tokens, resolves role via config rules, sets session cookie
- User is redirected to
/demo-authentication/with an active session
The core feature this demo illustrates. Two roles access the same Employee table endpoint but receive different fields in the response:
| Field | Admin | Viewer |
|---|---|---|
id |
visible | visible |
name |
visible | visible |
email |
visible | visible |
department |
visible | visible |
title |
visible | visible |
salary |
visible | denied |
ssn |
visible | denied |
homeAddress |
visible | denied |
personalEmail |
visible | denied |
Field masking is enforced server-side via attribute_permissions in the role definition. The denied fields are removed from the serialized response before it is sent -- they never leave the server. No application code is involved.
A single-page application built with React and Vite that provides a visual interface for the demo:
- Login panel -- switch between Basic auth, JWT, and OAuth login methods
- Employee data panel -- fetch employee records and see which fields are returned
- Role indicator -- shows current user, auth method, and assigned role
- Development mode notice -- warns when auth is bypassed in dev mode
The primary data table demonstrating attribute-level access control.
| Field | Type | Required | Sensitive | Description |
|---|---|---|---|---|
id |
String | Yes | No | Unique employee identifier (e.g., "emp-001") |
name |
String | Yes | No | Full name |
email |
String | Yes | No | Work email address |
department |
String | Yes | No | Department name |
title |
String | Yes | No | Job title |
salary |
Float | No | Yes | Annual salary (admin only) |
ssn |
String | No | Yes | Social security number (admin only) |
homeAddress |
String | No | Yes | Home address (admin only) |
personalEmail |
String | No | Yes | Personal email address (admin only) |
Seeded via authLoader on startup.
| Field | Value | Description |
|---|---|---|
username |
admin |
Admin user with full CRUD access |
password |
admin123 |
Hashed with Argon2id on load |
roleId |
admin |
References the admin role |
email |
admin@example.com |
User email |
username |
user |
Standard user with read-only access |
password |
user123 |
Hashed with Argon2id on load |
roleId |
viewer |
References the viewer role |
email |
user@example.com |
User email |
Seeded via authLoader on startup. Defines permissions per database, per table, per field.
| Role | Employee CRUD | Attribute Restrictions |
|---|---|---|
super_user |
Full (all databases) | None |
admin |
Read, Insert, Update, Delete | None -- all fields visible |
viewer |
Read only | salary, ssn, homeAddress, personalEmail denied |
App configuration lives in Cargo.toml. The [package.metadata.app] table covers the schema, seed loaders, and static SPA; the [package.metadata.auth] table declares which auth methods are enabled along with JWT, OAuth provider, and role-mapping settings. There is no separate config.yaml or services.yaml.
[package]
name = "demo-authentication"
version = "1.0.0"
description = "Role-based access control with Basic, JWT, and OAuth login"
[package.metadata.app]
schemas = "schemas/auth.graphql"
dataLoader = "data/employees.json"
static = { path = "web", source = "source", spa = true, build = "npm install && npm run build" }
[package.metadata.app.authLoader]
roles = "data/roles.json"
users = "data/users.json"
[package.metadata.auth]
methods = ["basic", "jwt", "oauth"]
[package.metadata.auth.jwt]
secret = "${JWT_SECRET}" # Set via environment variable
accessTtl = 900 # 15 minutes
refreshTtl = 604800 # 7 days
[package.metadata.auth.oauth.github]
clientId = "${GITHUB_CLIENT_ID}"
clientSecret = "${GITHUB_CLIENT_SECRET}"
[package.metadata.auth.oauth.google]
clientId = "${GOOGLE_CLIENT_ID}"
clientSecret = "${GOOGLE_CLIENT_SECRET}"
# Provider-based rules assign roles automatically on OAuth login.
[[package.metadata.auth.oauth.rules]]
strategy = "provider"
pattern = "google"
role = "admin" # Google users -> admin role
[[package.metadata.auth.oauth.rules]]
strategy = "provider"
pattern = "github"
role = "viewer" # GitHub users -> viewer role| Variable | Required | Description |
|---|---|---|
JWT_SECRET |
Yes | Secret key for HS256 JWT signing |
GITHUB_CLIENT_ID |
For OAuth | GitHub OAuth app client ID |
GITHUB_CLIENT_SECRET |
For OAuth | GitHub OAuth app client secret |
GOOGLE_CLIENT_ID |
For OAuth | Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
For OAuth | Google OAuth client secret |
Basic auth and JWT work without any environment variables (JWT will use a default secret in development mode). OAuth requires valid provider credentials.
The demo ships with three data files that are loaded on startup:
| File | Purpose | Records |
|---|---|---|
data/employees.json |
Employee records for the RBAC demo | 5 |
data/users.json |
Auth users (admin + user) | 2 |
data/roles.json |
Auth roles (super_user + admin + viewer) | 3 |
| Endpoint | Methods | Description |
|---|---|---|
/demo-authentication/Employee |
GET, POST | List/create employees |
/demo-authentication/Employee/{id} |
GET, PUT, DELETE | Read/update/delete an employee |
/demo-authentication/Employee?stream=sse |
GET | Real-time SSE stream of changes |
| Endpoint | Method | Description |
|---|---|---|
/yeti-auth/login |
POST | JWT login (username + password -> token pair) |
/yeti-auth/login |
DELETE | Logout (clear session cookie) |
/yeti-auth/jwt_refresh |
POST | Refresh an expired access token |
/yeti-auth/oauth_login |
GET | Initiate OAuth flow |
/yeti-auth/oauth_callback |
GET | OAuth provider redirect target |
/yeti-auth/oauth_user |
GET | Current OAuth session info |
/yeti-auth/oauth_logout |
POST | Clear OAuth session |
/yeti-auth/oauth_providers |
GET | List providers and role mappings |
/yeti-auth/auth |
GET | Auth status check |
demo-authentication/
├── Cargo.toml # App + auth configuration ([package.metadata.app|auth])
├── schemas/
│ └── auth.graphql # Employee table with sensitive fields
├── data/
│ ├── employees.json # 5 seed employee records
│ ├── users.json # 2 seed users (admin, user)
│ └── roles.json # 3 roles (super_user, admin, viewer)
├── source/ # React/Vite frontend
│ ├── index.html
│ ├── package.json
│ ├── vite.config.ts
│ └── src/
│ ├── main.tsx # React entry point
│ ├── App.tsx # Thin shell with navigation
│ ├── api.ts # Fetch helpers
│ ├── types.ts # Shared TypeScript types
│ ├── utils.ts # JSON syntax highlighting
│ ├── components/
│ │ ├── LoginPage.tsx # Demo's own auth method selector (Basic/JWT/OAuth)
│ │ └── Footer.tsx # Shared UI primitives
│ ├── hooks/
│ │ └── useAuth.ts # Auth state hook
│ ├── pages/
│ │ └── AuthPage.tsx # Login + employee data panels
│ └── styles/
│ ├── _vars.css # Per-app brand colors and shared tokens
│ ├── yeti.css # Canonical Yeti stylesheet
│ └── index.css # App-specific overrides
└── web/ # Built output (generated by npm run build)
This demo intentionally ships its own components/LoginPage.tsx instead of the standard pages/Login.tsx template -- LoginPage is the subject of the demo, with first-class UI for switching between Basic, JWT, and each OAuth provider. Other yeti UI apps use the simpler shared pages/Login.tsx + useAuth template that wraps the SPA with three lines:
const auth = useAuth()
if (auth === null) return <Loading/>
if (!auth) return <Login/>
return <App/>Otherwise the layout follows the standard yeti UI app structure: thin App.tsx, root utility modules (api.ts, types.ts, utils.ts), shared UI in components/, hooks in hooks/, pages in pages/, stylesheets in styles/ (yeti.css canonical, _vars.css per-app tokens, index.css overrides).
| demo-authentication | Typical Auth Setup | |
|---|---|---|
| Auth methods | Basic + JWT + OAuth from [package.metadata.auth] |
Each method requires separate library/middleware |
| Field masking | Declarative in role JSON | Custom serialization logic per endpoint |
| Role assignment | OAuth rules in Cargo.toml |
Manual user provisioning or custom mapping code |
| Seed data | authLoader + dataLoader in config |
Migration scripts or manual setup |
| Password hashing | Argon2id (OWASP params), automatic | Choose library, configure parameters, test |
| Token refresh | Built-in endpoint, automatic rotation | Custom refresh logic and token storage |
| Frontend | SPA with Vite, auto-built on deploy | Separate build pipeline and deployment |
| Lines of auth code | 0 (configuration only) | Hundreds to thousands |
Built with Yeti | The Performance Platform for Agent-Driven Development
