first: commit

manager
Guillermo Pages 2 months ago
commit 50fbb2224e

122
.gitignore vendored

@ -0,0 +1,122 @@
db_backups/
# build
build/
build-test/
# keys
.keys/
# vscode
.vscode/
# JWT
*RS256.key*
# nyc
.nyc_output
converage
*.lcov
# vim
*.swp
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
AuthKey_XSVS859WJA.p8
.DS_Store
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env.prod
.env.auth
.env.vault
# .pem files
pk*.pem
rsa*.pem
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Elastic Beanstalk Files
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml

@ -0,0 +1,17 @@
FROM node:20-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --only=production
# Copy source and build
COPY . .
RUN npm run build:prod
# Expose port
EXPOSE 3700
# Start the application
CMD ["node", "build/src/index.js"]

@ -0,0 +1,16 @@
FROM node:20-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm install
# Copy source
COPY . .
# Expose port
EXPOSE 3700
# Development mode with hot reload
CMD ["npm", "run", "dev"]

@ -0,0 +1,180 @@
# Quick Start Guide - playchoo-auth
## Prerequisites
1. **Redis** running locally or accessible
2. **SwissOID client credentials** (client ID + secret)
3. **Node.js 20+** installed
## Local Development Setup
### 1. Install Dependencies
```bash
npm install
```
### 2. Configure Environment
Copy `.env` and update with your values:
```bash
cp .env .env.local
# Edit .env.local with your SwissOID credentials
```
### 3. Prepare the shared Redis instance
```bash
# Ensure the shared network exists (no-op if it already does)
docker network create playchoo_redis_network || true
# Start the Redis service from py-playchoo-api (or attach your own instance to the same network)
(cd ../py-playchoo-api && docker compose -f docker-compose.dev.yml up redis)
```
> If you run the Node service directly on your machine, set `REDIS_URL` to
> `redis://localhost:6379`. Inside Docker the compose file rewrites it to
> `redis://redis:6379` automatically.
### 4. Start the Auth Service
```bash
npm run dev
```
The service will start on `http://localhost:3700`.
### 5. Test the Endpoints
**Health Check**:
```bash
curl http://localhost:3700/healthz
```
**Auth Status** (should return unauthenticated):
```bash
curl http://localhost:3700/auth/status
```
**Login** (open in browser):
```
http://localhost:3700/login
```
This will redirect to SwissOID, and after login, redirect back to your `RP_FRONTEND_URL`.
## Production Deployment
### 1. Build the Application
```bash
npm run build:prod
```
### 2. Update Production Environment
```bash
cp .env.prod .env.prod.local
# Edit .env.prod.local with production values
```
### 3. Deploy with Docker
```bash
docker-compose up -d
```
## Verify Integration
### Check Session Storage
After logging in, check Redis for your session:
```bash
# Connect to Redis
redis-cli
# List all session keys
KEYS session:*
# View a specific session
GET session:<your-session-id>
```
You should see JSON like:
```json
{
"sub": "user-uuid-from-swissoid",
"email": "user@example.com",
"iat": 1234567890,
"exp": 1234575090
}
```
### Test Cookie Setting
Use browser DevTools:
1. Open Network tab
2. Visit `http://localhost:3700/login`
3. Complete SwissOID flow
4. Check Application → Cookies
5. Verify `playchoo_session` cookie is set with:
- Domain: `.playchoo.com` (or `localhost` in dev)
- Secure: `true` (in production)
- HttpOnly: `true`
- SameSite: `None` (or `Lax`)
## Troubleshooting
### "Session not found" in API
- Verify Redis is running: `redis-cli ping`
- Check cookie name matches in both services: `SESSION_COOKIE_NAME`
- Ensure `REDIS_URL` is the same in auth service and API
### SwissOID callback fails
- Verify `OIDC_REDIRECT_BASE_URL` matches your registered redirect URI
- Check SwissOID client is configured for `authorization_code` flow
- Ensure `SWISSOID_CLIENT_SECRET` is correct
### Cookie not set
- Check `RP_COOKIE_DOMAIN` matches your domain structure
- For localhost, use `localhost` (no leading dot)
- For production, use `.playchoo.com` (with leading dot)
- Verify `CORS_ALLOWED_ORIGIN` includes your frontend URL
### CORS errors
- Add frontend URL to `CORS_ALLOWED_ORIGIN`
- Ensure frontend uses `credentials: 'include'` in fetch
- Check browser console for specific CORS error
## Environment Variables Reference
| Variable | Description | Example |
|----------|-------------|---------|
| `SWISSOID_CLIENT_ID` | SwissOID client identifier | `playchoo` |
| `SWISSOID_CLIENT_SECRET` | SwissOID client secret | `<secret>` |
| `SESSION_COOKIE_NAME` | Name of session cookie | `playchoo_session` |
| `SESSION_TTL` | Session lifetime (seconds) | `7200` (2 hours) |
| `REDIS_URL` | Redis connection string | `redis://redis:6379` |
| `RP_FRONTEND_URL` | Frontend redirect after login | `https://app.playchoo.com` |
| `RP_COOKIE_DOMAIN` | Cookie domain | `.playchoo.com` |
## Next Steps
1. **Integrate with py-playchoo-api**: See `/py-playchoo-api/README.md`
2. **Update Frontend**: Install `swissoid-front` and configure provider
3. **Configure Production Secrets**: Use a secrets manager for sensitive values
4. **Set up Monitoring**: Add alerts for Redis health, auth failures
5. **Test End-to-End**: Complete login flow from frontend → auth → API
## Support
For issues or questions, check:
- [swissoid-back README](../swissoid-back/README.md)
- [Integration Summary](../SWISSOID_INTEGRATION_SUMMARY.md)
- SwissOID documentation

@ -0,0 +1,122 @@
# playchoo-auth
SwissOID authentication service for Playchoo. Manages OIDC login flow, session storage in Redis, and provides authentication endpoints for the Playchoo stack.
## Architecture
- **SwissOID Integration**: Uses `swissoid-back` for OIDC authorization code flow
- **Session Storage**: Redis-backed sessions with HttpOnly cookies
- **Shared Sessions**: `api.playchoo.com` and `app.playchoo.com` share session state via Redis
## Endpoints
- `GET /login` - Initiates SwissOID OIDC flow
- `POST /oidc/callback` - Handles SwissOID callback
- `GET /auth/status` - Returns current authentication status
- `POST /auth/logout` - Destroys session
- `GET /healthz` - Health check
## Environment Variables
```env
# SwissOID Configuration
SWISSOID_CLIENT_ID=playchoo
SWISSOID_CLIENT_SECRET=<your-secret>
SWISSOID_ISSUER=https://api.swissoid.com
SWISSOID_JWKS_URI=https://api.swissoid.com/.well-known/jwks.json
SWISSOID_TOKEN_ENDPOINT=https://api.swissoid.com/token
SWISSOID_AUTHORIZE_ENDPOINT=https://api.swissoid.com/authorize
# Session Configuration
SESSION_COOKIE_NAME=playchoo_session
REFRESH_COOKIE_NAME=playchoo_refresh
SESSION_SECRET=<generate-random-secret>
STATE_SIGNING_SECRET=<generate-random-secret>
SESSION_TTL=7200
REFRESH_TTL=604800
# Redis (shared with py-playchoo-api)
REDIS_URL=redis://redis:6379
# RP Configuration
OIDC_REDIRECT_BASE_URL=https://auth.playchoo.com
RP_FRONTEND_URL=https://app.playchoo.com
RP_COOKIE_DOMAIN=.playchoo.com
POST_LOGIN_PATH=/dashboard
```
> When running the service directly on your host (outside Docker), override
> `REDIS_URL` to `redis://localhost:6379`. The Docker Compose files remap it to
> `redis://redis:6379` so the container can reach the shared Redis service.
## Local Development
```bash
npm install
# Ensure the shared Redis network exists (run once)
docker network create playchoo_redis_network || true
# Start the Redis service from py-playchoo-api (or attach your own instance to the same network)
(cd ../py-playchoo-api && docker compose -f docker-compose.dev.yml up redis)
# In a new terminal, start the auth service
npm run dev
```
The service will start on `http://localhost:3700` and will reuse the Redis
container from `py-playchoo-api` through the `playchoo_redis_network` network.
## Docker
```bash
# Development
docker-compose -f docker-compose.dev.yml up
# Production
docker-compose up
```
## Session Format
Sessions stored in Redis under key `session:<sessionId>`:
```json
{
"sub": "user-uuid-from-swissoid",
"email": "user@example.com",
"iat": 1234567890,
"exp": 1234575090
}
```
## Integration
### Python API (py-playchoo-api)
The Python API reads sessions from the same Redis instance:
```python
# Retrieve session from cookie
session_id = request.cookies.get('playchoo_session')
session_data = redis_client.get(f'session:{session_id}')
user_uuid = json.loads(session_data)['sub']
```
### Frontend (playchoo-nextjs)
Uses `swissoid-front` React provider:
```tsx
import { SwissOIDAuthProvider } from 'swissoid-front';
<SwissOIDAuthProvider baseUrl="https://auth.playchoo.com">
<App />
</SwissOIDAuthProvider>
```
## Related Documentation
- [`swissoid-back` README](../swissoid-back/README.md) - OIDC backend integration
- [`swissoid-front` README](../swissoid-front/README.md) - React frontend helpers
- [Playchoo API README](../py-playchoo-api/README.md) - Session lookup implementation

@ -0,0 +1,26 @@
version: '3.8'
services:
playchoo-auth:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3700:3700"
environment:
- NODE_ENV=development
- APP_PORT=3700
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
env_file:
- .env
volumes:
- .:/app
- /app/node_modules
networks:
- playchoo_redis_network
command: npm run dev
networks:
playchoo_redis_network:
name: playchoo_redis_network
external: true

@ -0,0 +1,42 @@
version: '3.8'
services:
playchoo-auth:
build: .
ports:
- "3700:3700"
env_file:
- .env.prod
environment:
NODE_ENV: ${NODE_ENV:-production}
APP_PORT: ${APP_PORT:-3700}
APPLICATION_NAME: ${APPLICATION_NAME:-Playchoo Auth}
CORS_ALLOWED_ORIGIN: ${CORS_ALLOWED_ORIGIN}
SWISSOID_CLIENT_ID: ${SWISSOID_CLIENT_ID}
SWISSOID_CLIENT_SECRET: ${SWISSOID_CLIENT_SECRET}
SWISSOID_ISSUER: ${SWISSOID_ISSUER}
SWISSOID_JWKS_URI: ${SWISSOID_JWKS_URI}
SWISSOID_TOKEN_ENDPOINT: ${SWISSOID_TOKEN_ENDPOINT}
SWISSOID_AUTHORIZE_ENDPOINT: ${SWISSOID_AUTHORIZE_ENDPOINT}
REDIS_URL: ${REDIS_URL}
SESSION_COOKIE_NAME: ${SESSION_COOKIE_NAME:-playchoo_session}
REFRESH_COOKIE_NAME: ${REFRESH_COOKIE_NAME:-playchoo_refresh}
SESSION_SECRET: ${SESSION_SECRET}
STATE_SIGNING_SECRET: ${STATE_SIGNING_SECRET}
SESSION_TTL: ${SESSION_TTL:-7200}
REFRESH_TTL: ${REFRESH_TTL:-604800}
OIDC_REDIRECT_BASE_URL: ${OIDC_REDIRECT_BASE_URL}
RP_FRONTEND_URL: ${RP_FRONTEND_URL}
RP_COOKIE_DOMAIN: ${RP_COOKIE_DOMAIN}
POST_LOGIN_PATH: ${POST_LOGIN_PATH:-/dashboard}
ALLOW_CONTINUE_PARAM: ${ALLOW_CONTINUE_PARAM:-true}
LOGGER_LOG: ${LOGGER_LOG:-1}
LOGGER_DEBUG: ${LOGGER_DEBUG:-0}
networks:
- playchoo_redis_network
restart: unless-stopped
networks:
playchoo_redis_network:
name: playchoo_redis_network
external: true

2114
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,31 @@
{
"name": "playchoo-auth",
"version": "1.0.0",
"description": "SwissOID authentication service for Playchoo",
"main": "build/src/index.js",
"type": "commonjs",
"scripts": {
"build": "tsc",
"build:prod": "tsc",
"start": "node build/src/index.js",
"dev": "ts-node src/index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": ["swissoid", "auth", "oidc"],
"author": "",
"license": "ISC",
"dependencies": {
"di-why": "^0.20.1",
"express": "^5.1.0",
"express-knifey": "^1.1.3",
"ioredis": "^5.8.1",
"swissoid-back": "^2.2.5",
"swiss-army-knifey": "^1.36.4"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.11.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}

@ -0,0 +1,36 @@
import { isMeantToBeTrue, UnknownEnv } from 'swiss-army-knifey';
type Env = UnknownEnv & {
APPLICATION_NAME?: string;
APP_PORT?: string;
NODE_ENV?: string;
CORS_ALLOWED_ORIGIN?: string;
CORS_CREDENTIALS?: string;
COOKIE_DOMAIN?: string;
SECURE_COOKIES?: string;
TRUST_PROXY?: string;
};
const appConfigMap = (env: Env) => ({
applicationName: env.APPLICATION_NAME || 'Playchoo Auth',
serverPort: (env.APP_PORT !== undefined && parseInt(env.APP_PORT, 10)) || 3700,
nodeEnv: env.NODE_ENV || 'development',
corsAllowedOrigin: env.CORS_ALLOWED_ORIGIN || 'http://localhost:3000',
corsCredentials:
env.CORS_CREDENTIALS !== undefined
? isMeantToBeTrue(env.CORS_CREDENTIALS)
: true,
cookieDomain: env.COOKIE_DOMAIN,
secureCookies:
env.SECURE_COOKIES !== undefined
? isMeantToBeTrue(env.SECURE_COOKIES)
: env.NODE_ENV === 'production',
trustProxy: env.TRUST_PROXY || '1',
healthCheckPath: '/healthz',
healthCheckResponse: 'ok',
});
export default appConfigMap;

@ -0,0 +1,30 @@
import 'dotenv/config';
import DiContainer from 'di-why/build/src/DiContainer';
import appConfigMap from './config/appConfigMap';
import { loadDict } from './loaders';
async function bootstrap() {
console.log('[Bootstrap] Starting Playchoo Auth Service...');
// Create DI container with all loaders
const diContainer = new DiContainer({
load: {
...loadDict,
// Override appConfig with our custom config
appConfig: {
factory: () => appConfigMap(process.env),
locateDeps: {},
},
},
});
// Start the Express server (includes all middleware, OIDC routes, health checks)
await diContainer.load('expressLauncher');
console.log('[Bootstrap] Playchoo Auth Service started successfully');
}
bootstrap().catch((error) => {
console.error('[Bootstrap] Failed to start Playchoo Auth Service:', error);
process.exit(1);
});

@ -0,0 +1,45 @@
import { mergeLDs } from 'di-why';
import expressLoadDict, {
EXPRESS_MIDDLEWARE,
buildMiddlewareConfig,
} from 'express-knifey';
import type { MiddlewareConfig, MiddlewarePathConfig } from 'express-knifey';
import { swissoidAuthLoadDict } from 'swissoid-back';
const middlewareConfig: MiddlewarePathConfig = buildMiddlewareConfig([
{
path: '*',
middleware: [
EXPRESS_MIDDLEWARE.trustProxy,
EXPRESS_MIDDLEWARE.cors,
EXPRESS_MIDDLEWARE.cookieParser,
EXPRESS_MIDDLEWARE.bodyParser,
EXPRESS_MIDDLEWARE.urlencoded,
],
},
{
path: EXPRESS_MIDDLEWARE.healthCheck.defaultPath ?? '/healthz',
middleware: [EXPRESS_MIDDLEWARE.healthCheck],
},
]);
const globalPath = '*';
const enhancedGlobal: MiddlewareConfig[] = [
...(middlewareConfig[globalPath] ?? []),
{ name: 'oidcStandardRoutesMiddleware', priority: 60 },
];
middlewareConfig[globalPath] = enhancedGlobal;
export const loadDict = mergeLDs(
expressLoadDict,
swissoidAuthLoadDict,
{
middlewareConfig: {
factory: () => middlewareConfig,
locateDeps: {},
},
}
);
export default loadDict;

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./build",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "build"]
}
Loading…
Cancel
Save