Files
phaseflow/specs/authentication.md
Petru Paler 2ae6804cc4
All checks were successful
Deploy / deploy (push) Successful in 1m37s
Fix OAuth login by syncing auth state to cookie
Root cause: PocketBase SDK stores auth in localStorage, but Next.js
middleware checks for pb_auth cookie. The cookie was never being set
after successful OAuth login.

Fix: Add pb.authStore.onChange() listener that syncs auth state to
cookie on any change (login, logout, token refresh). This is the
idiomatic PocketBase pattern for Next.js SSR apps.

Also updates authentication spec to reflect that the cookie is
non-HttpOnly by design (client SDK needs read/write access).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 13:59:53 +00:00

184 lines
4.5 KiB
Markdown

# Authentication Specification
## Job to Be Done
When I access PhaseFlow, I want to securely log in with my identity provider, so that my personal health data remains private.
## Auth Provider
Using PocketBase for authentication and data storage, with OIDC (Pocket-ID) as the primary identity provider.
**Connection:**
- `NEXT_PUBLIC_POCKETBASE_URL` environment variable
- `src/lib/pocketbase.ts` initializes client
## Login Flow
### OIDC Authentication (Pocket-ID)
1. User clicks "Sign In" on `/login`
2. App redirects to Pocket-ID authorization endpoint
3. User authenticates with Pocket-ID (handles MFA if configured)
4. Pocket-ID redirects back with authorization code
5. PocketBase exchanges code for tokens
6. User redirected to dashboard
### OIDC Configuration
Configure in PocketBase Admin UI: **Settings → Auth providers → OpenID Connect**
**Required Settings:**
- `Client ID` - From Pocket-ID application
- `Client Secret` - From Pocket-ID application
- `Issuer URL` - Your Pocket-ID instance URL (e.g., `https://id.yourdomain.com`)
**Environment Variables:**
```env
POCKETBASE_OIDC_CLIENT_ID=phaseflow
POCKETBASE_OIDC_CLIENT_SECRET=xxx
POCKETBASE_OIDC_ISSUER_URL=https://id.yourdomain.com
```
### Session Management
- PocketBase SDK stores auth in localStorage by default
- `pb.authStore.onChange()` listener syncs auth state to `pb_auth` cookie
- Cookie enables Next.js middleware to check auth server-side
- Cookie is non-HttpOnly (PocketBase SDK requires client-side access)
- Session expires after 14 days of inactivity
## Pages
### `/login`
**Elements:**
- "Sign In with Pocket-ID" button
- Error message display
**Behavior:**
- Redirect to `/` on successful login
- Show error message on failed attempt
- Rate limit: 5 attempts per minute
### Protected Routes
All routes except `/login` require authentication.
**Middleware Check:**
1. Check for valid PocketBase auth token
2. If invalid/missing, redirect to `/login`
3. If valid, proceed to requested page
## API Authentication
### User Context
API routes access current user via:
```typescript
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL);
// Auth token from request cookies
const user = pb.authStore.model;
```
### Protected Endpoints
All `/api/*` routes except:
- `/api/calendar/[userId]/[token].ics` (token-based auth)
- `/api/cron/*` (CRON_SECRET auth)
## User API
### GET `/api/user`
Returns current authenticated user profile.
**Response:**
```json
{
"id": "user123",
"email": "user@example.com",
"garminConnected": true,
"cycleLength": 31,
"lastPeriodDate": "2024-01-01",
"notificationTime": "07:00",
"timezone": "America/New_York"
}
```
### PATCH `/api/user`
Updates user profile fields.
**Request Body (partial update):**
```json
{
"cycleLength": 28,
"notificationTime": "06:30"
}
```
## User Schema
See `src/types/index.ts` for full `User` interface.
**Auth-related fields:**
- `id` - PocketBase record ID
- `email` - Login email
**Profile fields:**
- `cycleLength` - Personal cycle length (days)
- `notificationTime` - Preferred notification hour
- `timezone` - User's timezone
## PocketBase Client (`src/lib/pocketbase.ts`)
**Exports:**
- `pb` - Initialized PocketBase client
- `getCurrentUser()` - Get authenticated user
- `isAuthenticated()` - Check auth status
## Settings Page (`/settings`)
User profile management:
- View/edit cycle length
- View/edit notification time
- View/edit timezone
- Link to Garmin settings
## Success Criteria
1. OIDC login flow completes in under 5 seconds (including redirect)
2. Session persists across browser refreshes
3. Unauthorized access redirects to login
4. User data isolated by authentication
## Acceptance Tests
- [ ] OIDC redirect initiates correctly
- [ ] Successful OIDC callback creates/updates user
- [ ] Session persists after page refresh
- [ ] Protected routes redirect when not authenticated
- [ ] GET `/api/user` returns current user data
- [ ] PATCH `/api/user` updates user record
- [ ] Logout clears session completely
- [ ] Auth cookie syncs via onChange listener (non-HttpOnly per PocketBase design)
## Future Enhancements
### Open Registration
When open registration is enabled:
- Add `/signup` page with OIDC provider selection
- New users created automatically on first OIDC login
- Admin approval workflow (optional)
### Additional OAuth2 Providers
PocketBase supports multiple OAuth2 providers. Future options:
- Google
- GitHub
- Apple
- Other OIDC-compliant providers
Each provider configured separately in PocketBase Admin UI.