← Back to User Management

# 🔐 Access Manager

User management, scoped access control, and multi-method authentication for scottfelten.com services.

v2.0 — Live 18 Services 3 Auth Methods March 2026

## 1 Overview

Access Manager is the identity and access layer for all scottfelten.com services. It controls who can sign in, what they can access, and how they authenticate — all from a single dashboard.

#### 🔵 Google SSO

Sign in with Google. Primary method for team members and close collaborators. Powered by oauth2-proxy.

#### ✨ Magic Link

One-time login link sent to email. No password needed. Great for clients and external partners.

#### 🔑 Email + Password

Traditional login with bcrypt hashing. 12-char minimum. Lockout protection. Password reset via email.

💡 How it works: Every scottfelten.com service checks with Access Manager before letting anyone in. When a user visits any service, Access Manager verifies their session and checks if they have permission for that specific service.

## 2 Adding a User

1. Click "+ Add User" on the dashboard
   Opens the user creation modal with all configuration options.
2. Enter their email and name
   Email is their login identifier. Must be unique across all users.
3. Choose their role
   Client — external user, limited access.
   Partner — business collaborator (e.g., Colum for IAG).
   Admin — full access to everything including this dashboard.
4. Select auth method(s)
   Pick one or more: Google (they need a Google account), Magic Link (any email works), or Password (you set their initial password, or they can set it later via reset).
5. Assign services
   Check individual services they should access, or select All Services (*) for unrestricted access. Services are auto-populated from the project inventory — always current.
6. Optional: Add tags
   Comma-separated labels for organization (e.g., "partner, bpo-expert"). Purely informational for now.

⚡ Quick example — Adding Colum (IAG partner):
Email: colum@whatever.com · Role: Partner · Auth: Magic Link · Services: iag-website, iag-data-viewer, intellicert, usecasegen

## 3 Auth Methods — How Each Works

### 🔵 Google SSO

The primary auth method. Uses oauth2-proxy to handle the Google OAuth flow. When a user visits a service:

- They're redirected to Google's sign-in page
- After authenticating, oauth2-proxy sets a session cookie
- Access Manager verifies the cookie and checks their service permissions

Requirement: User must have a Google account, and their email must be in the allowlist.

### ✨ Magic Link

Email-based, passwordless login. Perfect for clients and partners who don't use Google.

- User enters their email on the login page
- They receive a one-time link (expires in 15 minutes)
- Clicking the link sets a session cookie valid for 7 days
- No password to remember, no account to create

🔒 Security: Links are single-use (consumed on first click) and time-limited. The token is a 32-byte random hex string — not guessable.

### 🔑 Email + Password

Traditional login for users who prefer (or need) a persistent credential.

- Passwords hashed with bcrypt (12 rounds) — industry standard
- 12-character minimum — enforced on creation and reset
- Lockout: 5 failed attempts → account locked for 30 minutes (auto-unlocks)
- Password reset: sends a time-limited reset link (1 hour expiry)

#### Setting a Password

Two ways to set a password for a user:

- At creation: Enter a password in the "Initial Password" field when adding the user
- After creation: Click the 🔑 button next to their name in the dashboard

Users can also reset their own password via the "Forgot password?" link on the login page.

## 4 Services & Permissions

Services are auto-discovered from the project inventory every 5 minutes. When a new service is deployed, it appears in the services list automatically — no manual updates needed.

### How Scoping Works

- Each user has a `services[]` array listing which services they can access
- When they visit a service, Access Manager maps the hostname to a service ID and checks if it's in their list
- Wildcard: `*` grants access to everything (admin default)

### Current Service Map

These services are protected by Access Manager. The list updates automatically from inventory.

Service | URL | Category
--- | --- | ---
inventory | inv.scottfelten.com | Core
access-manager | users.scottfelten.com | Core
taos | goalstack.scottfelten.com | Core
signal-tower | signal.scottfelten.com | Intelligence
customer-360 | c360.scottfelten.com | Sales
models-embeddings | models.scottfelten.com | AI/ML
iag-website | intelligenceadvisorygroup.com | IAG
iag-data-viewer | iag.scottfelten.com | IAG
intellicert | intellicert.app | IAG
usecasegen | app.usecasegen.app | IAG
+ 8 more (auto-discovered from inventory)

### Manual Sync

Click the ↻ Sync button in the dashboard header to force a refresh from inventory. Normally happens automatically every 5 minutes.

## 5 The Login Page

Available at /login.html — this is the public entry point for non-Google users. It's accessible without authentication.

### Three Tabs

- Google — redirects to Google sign-in (for users with Google SSO enabled)
- Magic Link — enter email, receive login link
- Password — enter email + password, with "Forgot password?" reset flow

### Redirect Flow

When a user hits a protected service without a session, they'll be redirected to the login page with a `?rd=` parameter. After successful auth, they're sent back to the service they originally wanted.

💡 Currently: The default redirect for unauthenticated users goes to Google sign-in. To send non-Google users to the multi-auth login page instead, share the direct link: `https://users.scottfelten.com/login.html?rd=https://SERVICE_URL`

## 6 Security

Feature | Detail
--- | ---
Password hashing | bcrypt, 12 rounds
Password minimum | 12 characters
Login lockout | 5 failed attempts → 30-minute lock (auto-unlocks)
Magic link expiry | 15 minutes, single-use
Reset token expiry | 1 hour, single-use
Session duration | 7 days (JWT cookie on .scottfelten.com)
Cookie flags | httpOnly, secure, sameSite=lax
Token entropy | 32 bytes (256-bit) random hex
Email enumeration | Prevented — all responses are identical regardless of email existence

### Session Types

Two session mechanisms coexist:

- oauth2-proxy cookie — for Google SSO users (managed by oauth2-proxy)
- _am_session cookie — for magic link and password users (JWT, managed by Access Manager)

The `/auth/check` endpoint tries the `_am_session` first, then falls back to oauth2-proxy. Either way, scoped access is enforced.

## 7 API Reference

All admin endpoints require authentication (Google SSO or _am_session with admin role).

### Authentication

POST /auth/magic/request — Request a magic link

Public endpoint. Sends a login link to the provided email (if registered).

POST /auth/magic/request // Body: { "email": "user@example.com", "redirect": "https://inv.scottfelten.com" } // Response (always the same, prevents email enumeration): { "ok": true, "message": "If that email is registered, a login link has been sent." }
POST /auth/password/login — Email + password login

Public endpoint. Returns session cookie on success.

POST /auth/password/login // Body: { "email": "user@example.com", "password": "their-password" } // Success: { "ok": true, "user": { "email": ..., "name": ..., "role": ... } } // Failure (with lockout tracking): { "error": "Invalid email or password", "attemptsRemaining": 3 } // Locked out: { "error": "Account locked. Try again in 28 minutes.", "locked": true }
POST /auth/password/set — Set password for a user (admin)
POST /auth/password/set // Body: { "email": "user@example.com", "password": "min-12-characters" }
POST /auth/password/reset-request — Request password reset

Public endpoint. Sends a reset link to the email if registered.

POST /auth/password/reset-request { "email": "user@example.com" }
POST /auth/password/reset — Execute password reset
POST /auth/password/reset { "token": "hex-token-from-email", "password": "new-password-12+" }
GET /auth/session — Check current session

Public endpoint. Returns session info or `{ authenticated: false }`.

POST /auth/logout — Clear session

Clears the `_am_session` cookie. Note: does not clear the oauth2-proxy session (Google SSO).

### User Management (Admin)

GET /api/users — List all users

Returns array of users. Password hashes are excluded; includes `hasPassword: true/false`.

POST /api/users — Create a user
POST /api/users { "email": "colum@example.com", "name": "Colum", "role": "partner", "authMethods": ["magic-link"], "services": ["iag-website", "iag-data-viewer", "intellicert", "usecasegen"], "tags": ["partner"], "password": "optional-initial-password" }
PUT /api/users/:email — Update a user

Partial update — only send the fields you want to change.

PUT /api/users/colum@example.com { "services": ["iag-website", "intellicert"] }
DELETE /api/users/:email — Remove a user

Removes the user and updates the oauth2-proxy allowlist. Cannot delete yourself.

### Services

GET /api/services — List available services

Returns sorted array of unique service IDs (deduplicated from hostname mappings).

GET /api/services/map — Full hostname → service map

Returns the complete mapping of hostnames to service IDs. Useful for debugging.

POST /api/services/sync — Force sync from inventory

Pulls fresh data from inventory API. Normally happens automatically every 5 minutes.

### Configuration

PUT /api/config/email — Configure email transport
PUT /api/config/email { "host": "smtp.gmail.com", "port": 587, "user": "scott@scottfelten.com", "pass": "google-app-password", "from": "TARS <scott@scottfelten.com>" }

## 8 Architecture

### Auth Flow

User visits service.scottfelten.com ↓ nginx auth_request → Access Manager /auth/check ↓ Check 1: _am_session cookie? (magic link / password users) ↓ no Check 2: oauth2-proxy session? (Google SSO users) ↓ no 401 → redirect to Google sign-in (or login page for non-Google users) ↓ yes (either check) Look up user → check service scope → 200 (allow) or 403 (denied)

### Infrastructure

Component | Location | Port
--- | --- | ---
Access Manager | VPS systemd: access-manager.service | 3030
oauth2-proxy | VPS systemd: oauth2-proxy.service | 4180
Inventory API | VPS systemd: inventory-api.service | 3025
nginx | All services proxied, auth_request on each | 443

### Data Files

File | Purpose
--- | ---
`data/users.json` | User store (email, role, services, passwordHash, authMethods)
`data/services-cache.json` | Cached service map from inventory (refreshed every 5 min)
`data/tokens.json` | Active magic link and reset tokens (auto-cleaned)
`data/lockouts.json` | Failed login tracking and lockout state
`data/config.json` | JWT secret, SMTP config, email settings
`/etc/oauth2-proxy-users.txt` | oauth2-proxy email allowlist (auto-synced from users.json)

## 9 Setting Up Email

Email is required for magic links and password resets. Without it, these features log to the server console (useful for testing, not for real users).

### Option A: Google Workspace SMTP (Recommended)

1. Create a Google App Password
   Go to myaccount.google.com/apppasswords → generate a password for "Mail" on "Other device"
2. Configure via API
   PUT https://users.scottfelten.com/api/config/email { "host": "smtp.gmail.com", "port": 587, "user": "scott@scottfelten.com", "pass": "xxxx-xxxx-xxxx-xxxx", "from": "TARS <scott@scottfelten.com>" }
3. Test it
   Request a magic link for your own email. If it arrives, you're good.

### Option B: Resend.com

Free tier: 100 emails/day. Use `smtp.resend.com` with your API key as the password.

## 10 Quick Reference

Task | How
--- | ---
Add a Google SSO user | Dashboard → + Add User → select Google auth + services
Add a non-Google user | Dashboard → + Add User → select Magic Link or Password auth
Set/change a password | Dashboard → click 🔑 button next to user
See who has access to what | Dashboard — services shown inline per user
Refresh the services list | Dashboard → ↻ Sync button
Send a login link | Share: `users.scottfelten.com/login.html`
Check system health | `GET /health` — shows users, services, email status
Unlock a locked account | Wait 30 min (auto-unlocks) or delete `data/lockouts.json`
Configure email | `PUT /api/config/email` (see Section 9)

Access Manager v2.0 · Built by TARS · March 2026