Skip to content

Commit cb79f2d

Browse files
committed
Add authentication system with OpenID Connect and role-based authorization
Auth Core (lib/beacon/auth/): - User schema: email, name, hashed_password (dev mode), avatar_url, login tracking - UserSession schema: token-based sessions with crypto random tokens - UserRole schema: super_admin, site_admin, site_editor, site_viewer roles with site scoping (nil site = platform-wide) - Auth context (lib/beacon/auth.ex): user CRUD, password management, session management, role assignment/checking, OIDC authentication - Auth config (lib/beacon/auth/config.ex): dev_mode?, providers, session settings OIDC Integration: - OIDCController: authorize redirect + callback with token exchange - Uses openid_connect hex package for discovery, token verification, claims - Multiple providers supported (Google, Azure AD, Okta, etc.) - Pre-provisioned users only — OIDC login rejects unknown emails Dev Mode: - DevLoginController: email + password login for development - Disabled in production (guarded by dev_mode? config) - Passwords hashed via bcrypt_elixir Authorization Plugs: - RequireAuth: session cookie check, user loading, login redirect - RequireRole: role-based access control with site scoping Mix Task: - mix beacon.create_admin --email X --name Y [--password Z] - Creates super_admin user for initial setup Migration V013: beacon_users, beacon_user_sessions, beacon_user_roles tables Dependencies: openid_connect ~> 1.0, bcrypt_elixir ~> 3.0
1 parent 9be4dbb commit cb79f2d

16 files changed

Lines changed: 1191 additions & 1 deletion

beacon-admin-auth-design.md

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
# Beacon Admin & Authentication System Design
2+
3+
## Problem
4+
5+
Beacon has no user management, no authentication, and no authorization. The LiveAdmin is unprotected. There's no Beacon-level admin for managing the platform. The site-level admin can't distinguish between admins, editors, and viewers.
6+
7+
## Authentication: OpenID Connect
8+
9+
Beacon uses **OpenID Connect** (via the `openid_connect` hex package) as its default authentication method. This delegates login, password management, MFA, and session lifecycle to an external identity provider (Google, Auth0, Okta, Keycloak, Azure AD, etc.).
10+
11+
### Auth Modes
12+
13+
**OIDC Mode** (default for production): One or more OIDC providers configured. Login redirects to the provider's authorization endpoint. On callback, Beacon matches the authenticated email against pre-provisioned `beacon_users` records.
14+
15+
**Dev Mode** (development/test only): Simple email + password login without OIDC. Allows local development without configuring an identity provider. Passwords stored in `beacon_users` via bcrypt. **Disabled in production.**
16+
17+
### Key Design Decisions
18+
19+
- **Pre-provisioned users only.** OIDC login does NOT auto-create Beacon users. An admin must create the user in Beacon first. If no matching `beacon_users` record exists for the authenticated email, login is rejected with "Account not found."
20+
- **Multiple OIDC providers.** Beacon supports a list of providers. The login page shows a button per provider. Users are matched by email across providers.
21+
- **Email is the identity key.** A user authenticated via Google as `jane@company.com` maps to the same Beacon user as one authenticated via Azure AD as `jane@company.com`.
22+
23+
### Configuration
24+
25+
```elixir
26+
config :beacon, :auth,
27+
# Production: OIDC providers
28+
providers: [
29+
google: [
30+
discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration",
31+
client_id: System.get_env("GOOGLE_CLIENT_ID"),
32+
client_secret: System.get_env("GOOGLE_CLIENT_SECRET"),
33+
redirect_uri: "https://mysite.com/admin/auth/google/callback",
34+
response_type: "code",
35+
scope: "openid email profile"
36+
],
37+
azure_ad: [
38+
discovery_document_uri: "https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration",
39+
client_id: System.get_env("AZURE_CLIENT_ID"),
40+
client_secret: System.get_env("AZURE_CLIENT_SECRET"),
41+
redirect_uri: "https://mysite.com/admin/auth/azure_ad/callback",
42+
response_type: "code",
43+
scope: "openid email profile"
44+
]
45+
],
46+
# Dev mode: enable password login (MUST be false in production)
47+
dev_mode: Mix.env() != :prod,
48+
# Session settings
49+
session_signing_salt: "beacon_auth",
50+
session_max_age: 86400 * 30 # 30 days
51+
52+
# openid_connect library config (referenced by Beacon)
53+
config :openid_connect, :providers,
54+
google: [
55+
discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration",
56+
client_id: System.get_env("GOOGLE_CLIENT_ID"),
57+
client_secret: System.get_env("GOOGLE_CLIENT_SECRET"),
58+
response_type: "code",
59+
scope: "openid email profile"
60+
]
61+
```
62+
63+
### OIDC Flow
64+
65+
```
66+
1. User visits /admin → not authenticated → redirect to login page
67+
2. Login page shows buttons: "Sign in with Google", "Sign in with Azure AD"
68+
3. User clicks provider → redirect to provider's authorization URL
69+
4. Provider authenticates user → redirects back to /admin/auth/:provider/callback
70+
5. Beacon exchanges code for tokens via openid_connect library
71+
6. Beacon extracts email from ID token claims
72+
7. Beacon looks up email in beacon_users table
73+
8. If found → create session, redirect to admin dashboard
74+
9. If not found → show "Account not found. Contact your administrator."
75+
```
76+
77+
### Dev Mode Flow
78+
79+
```
80+
1. User visits /admin → not authenticated → redirect to login page
81+
2. Login page shows email + password form (no OIDC buttons)
82+
3. User submits credentials
83+
4. Beacon verifies password against beacon_users.hashed_password
84+
5. If valid → create session, redirect to admin dashboard
85+
```
86+
87+
## Authorization: Role Hierarchy
88+
89+
| Role | Scope | Capabilities |
90+
|------|-------|-------------|
91+
| **Super Admin** | Platform-wide | Manage sites, global template types, global settings, assign any role, manage all site content |
92+
| **Site Admin** | One or more sites | Full control over assigned sites. Assign Editor/Viewer roles for their sites. |
93+
| **Site Editor** | One or more sites | Create/edit/publish pages, manage content. Cannot change settings or template types. |
94+
| **Site Viewer** | One or more sites | Read-only admin access. Cannot modify anything. |
95+
96+
## Data Model
97+
98+
### Migration V013
99+
100+
```sql
101+
CREATE TABLE beacon_users (
102+
id BINARY_ID PRIMARY KEY,
103+
email TEXT NOT NULL UNIQUE,
104+
name TEXT,
105+
hashed_password TEXT, -- Only used in dev mode
106+
avatar_url TEXT, -- From OIDC profile claims
107+
last_login_at TIMESTAMPTZ,
108+
last_login_provider TEXT, -- "google", "azure_ad", "dev"
109+
inserted_at TIMESTAMPTZ,
110+
updated_at TIMESTAMPTZ
111+
);
112+
113+
CREATE TABLE beacon_user_sessions (
114+
id BINARY_ID PRIMARY KEY,
115+
user_id BINARY_ID NOT NULL REFERENCES beacon_users(id) ON DELETE CASCADE,
116+
token BYTEA NOT NULL UNIQUE,
117+
inserted_at TIMESTAMPTZ NOT NULL
118+
);
119+
120+
CREATE INDEX ON beacon_user_sessions (token);
121+
122+
CREATE TABLE beacon_user_roles (
123+
id BINARY_ID PRIMARY KEY,
124+
user_id BINARY_ID NOT NULL REFERENCES beacon_users(id) ON DELETE CASCADE,
125+
role TEXT NOT NULL, -- "super_admin", "site_admin", "site_editor", "site_viewer"
126+
site TEXT, -- NULL for super_admin, site atom string for site-scoped
127+
inserted_at TIMESTAMPTZ,
128+
UNIQUE(user_id, role, site)
129+
);
130+
```
131+
132+
### Schemas
133+
134+
**`Beacon.Auth.User`**
135+
- Fields: email, name, hashed_password (optional), avatar_url, last_login_at, last_login_provider
136+
- Changeset: required email, unique email, password hashing (dev mode only)
137+
138+
**`Beacon.Auth.UserSession`**
139+
- Fields: user_id, token
140+
- Token is a random 32-byte binary, stored as a cookie
141+
142+
**`Beacon.Auth.UserRole`**
143+
- Fields: user_id, role, site
144+
- Validates role is one of: super_admin, site_admin, site_editor, site_viewer
145+
- super_admin roles must have site=nil
146+
- Site-scoped roles must have a non-nil site
147+
148+
### Context Module: `Beacon.Auth`
149+
150+
```elixir
151+
# User management
152+
Beacon.Auth.create_user(attrs) # Create a pre-provisioned user
153+
Beacon.Auth.update_user(user, attrs) # Update user profile
154+
Beacon.Auth.delete_user(user) # Delete user and all roles
155+
Beacon.Auth.list_users(opts) # List all users
156+
Beacon.Auth.get_user_by_email(email) # Look up by email
157+
158+
# Dev mode password
159+
Beacon.Auth.set_password(user, password) # Set password for dev mode
160+
Beacon.Auth.verify_password(user, password) # Verify password
161+
162+
# Session management
163+
Beacon.Auth.create_session(user) # Creates session token, returns token
164+
Beacon.Auth.get_user_by_session_token(token) # Look up user from session
165+
Beacon.Auth.delete_session(token) # Logout
166+
167+
# Role management
168+
Beacon.Auth.assign_role(user, role, site \\ nil)
169+
Beacon.Auth.revoke_role(user, role, site \\ nil)
170+
Beacon.Auth.list_roles(user)
171+
Beacon.Auth.has_role?(user, role, site \\ nil)
172+
Beacon.Auth.authorize!(user, action, site) # Raises if unauthorized
173+
174+
# OIDC
175+
Beacon.Auth.authenticate_oidc(provider, claims) # Match OIDC claims to user
176+
```
177+
178+
## Beacon Admin Interface
179+
180+
### URL Structure
181+
182+
```
183+
/admin/auth/login — Login page (OIDC buttons + dev mode form)
184+
/admin/auth/:provider/callback — OIDC callback
185+
/admin/auth/logout — Logout
186+
187+
/admin/beacon/ — Beacon dashboard (super_admin only)
188+
/admin/beacon/sites — Manage sites
189+
/admin/beacon/template_types — Global template types
190+
/admin/beacon/settings — Global settings
191+
/admin/beacon/users — User management + role assignment
192+
193+
/admin/:site/pages — Site-scoped (existing LiveAdmin)
194+
/admin/:site/seo — Site-scoped
195+
...
196+
```
197+
198+
### Beacon Admin Pages (Super Admin Only)
199+
200+
**Dashboard** — Overview of all sites, user counts, recent activity
201+
202+
**Sites** — List configured sites with page counts, endpoints, prefixes
203+
204+
**Global Template Types** — Same as existing template_type_manager but with `site: nil`
205+
206+
**Global Settings** — AI crawler policy defaults, default meta tags, default OG image
207+
208+
**Users** — CRUD for pre-provisioned users:
209+
- Create: email, name (password field only in dev mode)
210+
- Edit: name, avatar
211+
- Role assignment: multi-select roles with site picker for site-scoped roles
212+
- Deactivate/delete
213+
214+
### Auth Integration in LiveAdmin
215+
216+
**New Plugs:**
217+
218+
`Beacon.Auth.Plug.RequireAuth` — Checks session cookie, loads user, assigns to conn. Redirects to login if not authenticated.
219+
220+
`Beacon.Auth.Plug.RequireRole` — Checks user's roles against the requested scope. Returns 403 if insufficient permissions.
221+
222+
**LiveAdmin Router Changes:**
223+
224+
```elixir
225+
# In the host app's router
226+
beacon_live_admin "/admin",
227+
auth: true # Enables auth plugs in the admin pipeline
228+
```
229+
230+
When `auth: true`, the admin pipeline includes:
231+
1. `Beacon.Auth.Plug.RequireAuth` — redirect to login if no session
232+
2. `Beacon.Auth.Plug.LoadRoles` — load user's roles into assigns
233+
3. Route-level checks: Beacon admin pages require super_admin
234+
4. Site pages check site-scoped roles
235+
236+
### OIDC Controller
237+
238+
**`Beacon.Auth.OIDCController`** — Handles the OAuth2/OIDC redirect flow:
239+
240+
```elixir
241+
# GET /admin/auth/:provider — Redirect to OIDC provider
242+
def authorize(conn, %{"provider" => provider})
243+
244+
# GET /admin/auth/:provider/callback — Handle OIDC callback
245+
def callback(conn, %{"provider" => provider, "code" => code})
246+
```
247+
248+
Uses the `openid_connect` library to:
249+
1. Build authorization URL
250+
2. Exchange code for tokens
251+
3. Verify ID token
252+
4. Extract email from claims
253+
5. Match to beacon_users
254+
255+
## Implementation Phases
256+
257+
### Phase 1: Data Layer + Auth Context
258+
- Migration V013 (users, sessions, roles tables)
259+
- User, UserSession, UserRole schemas
260+
- Beacon.Auth context with all functions
261+
- bcrypt_elixir dependency for dev mode passwords
262+
- openid_connect dependency
263+
264+
### Phase 2: OIDC Flow + Session Management
265+
- OIDCController (authorize + callback)
266+
- Session plug (cookie-based, token lookup)
267+
- Login LiveView (provider buttons + dev mode form)
268+
- Logout controller
269+
270+
### Phase 3: Authorization Plugs
271+
- RequireAuth plug
272+
- RequireRole plug
273+
- Integration into LiveAdmin router pipeline
274+
275+
### Phase 4: Beacon Admin UI
276+
- Dashboard, Sites, Global Template Types, Global Settings, Users pages
277+
- Route mounting at /admin/beacon/ prefix
278+
- Super admin navigation
279+
280+
### Phase 5: Site Admin Role Enforcement
281+
- Existing LiveAdmin pages get role checks
282+
- UI adaptations for different roles (hide buttons for insufficient permissions)
283+
- Read-only mode for viewers
284+
285+
### Phase 6: First User Bootstrap
286+
- Mix task: `mix beacon.create_admin --email admin@example.com`
287+
- Creates super_admin user with password (for initial setup in dev mode)
288+
- In production: create user via mix task, first OIDC login links to that user
289+
290+
## Dependencies
291+
292+
- `openid_connect` — OIDC discovery, token exchange, claims verification
293+
- `bcrypt_elixir` — Password hashing for dev mode only
294+
295+
## Bootstrap Flow
296+
297+
**First deployment:**
298+
1. Run migrations (V013 creates tables)
299+
2. Run `mix beacon.create_admin --email admin@example.com --password secret` (dev mode) or just `--email` (OIDC mode)
300+
3. Configure OIDC provider in config
301+
4. First login via OIDC matches the pre-provisioned email → user gains super_admin access
302+
5. Super admin creates more users and assigns roles via the Beacon admin UI

0 commit comments

Comments
 (0)