# Milestone 4: Authentication System **Status**: Decided **Goal**: Secure developer authentication and app signing infrastructure. ## Decision **Custom JWT + OAuth2** with Go standard library crypto: ``` OAuth2: golang.org/x/oauth2 (GitHub, Google) JWT: github.com/golang-jwt/jwt/v5 Signing: crypto/ed25519 (stdlib) Password Hash: golang.org/x/crypto/argon2 API Key Hash: golang.org/x/crypto/bcrypt ``` ### Rationale 1. **Go stdlib crypto** - Ed25519 built into Go, no external deps 2. **Simple JWT** - golang-jwt is battle-tested, minimal 3. **Stateless tokens** - No token store needed (SQLite handles refresh token revocation) 4. **OAuth-first** - GitHub OAuth for most developers, minimal password handling --- ## Overview Authentication covers two areas: 1. **Developer authentication** - Login to portal, API access 2. **App signing** - Package integrity and developer verification --- ## Developer Authentication ### Methods Required | Method | Use Case | Priority | |--------|----------|----------| | OAuth2 (GitHub) | Primary login | P0 | | OAuth2 (Google) | Alternative login | P1 | | Email + Password | Fallback | P2 | | API Keys | CLI tools, CI/CD | P0 | ### OAuth2 Flow ``` ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐ │ Browser │────►│ Portal │────►│ Provider │────►│ Callback│ └─────────┘ └─────────┘ │(GitHub) │ └────┬────┘ └──────────┘ │ ▼ ┌─────────────┐ │ Create/Link │ │ Account │ └─────────────┘ ``` ### OAuth2 Implementation #### GitHub OAuth ``` Authorization URL: https://github.com/login/oauth/authorize Token URL: https://github.com/login/oauth/access_token User Info: https://api.github.com/user Scopes: read:user, user:email ``` #### Google OAuth ``` Authorization URL: https://accounts.google.com/o/oauth2/v2/auth Token URL: https://oauth2.googleapis.com/token User Info: https://www.googleapis.com/oauth2/v2/userinfo Scopes: openid, email, profile ``` ### Session Management #### JWT Tokens ```json { "sub": "dev_uuid", "email": "dev@example.com", "iat": 1704067200, "exp": 1704153600, "type": "access" } ``` | Token Type | Lifetime | Storage | |------------|----------|---------| | Access Token | 1 hour | Memory/Cookie | | Refresh Token | 30 days | HttpOnly Cookie | #### Token Refresh Flow ``` 1. Access token expires 2. Client sends refresh token 3. Server validates refresh token 4. Issue new access + refresh tokens 5. Invalidate old refresh token ``` ### API Key Authentication #### Key Format ``` mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx │ │ └── 32 random bytes (base62) │ └── Environment (live/test) └── Prefix (mosis key) ``` #### Key Storage ```sql -- Only store hash, never the key itself INSERT INTO api_keys ( developer_id, name, key_hash, -- bcrypt or argon2 key_prefix, -- "mk_live_abc" for display permissions ) VALUES (...); ``` #### Key Permissions ```json { "permissions": [ "apps:read", "apps:write", "versions:upload", "telemetry:read" ] } ``` ### Rate Limiting | Endpoint Category | Limit | Window | |-------------------|-------|--------| | Auth endpoints | 10 | 1 minute | | API (authenticated) | 1000 | 1 hour | | API (per key) | 100 | 1 minute | | Upload | 10 | 1 hour | --- ## App Signing ### Key Generation #### Algorithm: Ed25519 ``` Private key: 32 bytes (256 bits) Public key: 32 bytes (256 bits) Signature: 64 bytes (512 bits) ``` #### Why Ed25519? - Fast signing and verification - Small key and signature sizes - No configuration choices (secure by default) - Widely supported ### Key Generation Flow ```bash # Developer generates keypair locally mosis keys generate # Output: # Private key saved to: ~/.mosis/signing_key.pem # Public key saved to: ~/.mosis/signing_key.pub # # Fingerprint: SHA256:xxxxx # # IMPORTANT: Keep your private key secure! # Upload your public key to the developer portal. ``` ### Key Format #### Private Key (PEM) ``` -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIGxxxxx... -----END PRIVATE KEY----- ``` #### Public Key (PEM) ``` -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAxxxxx... -----END PUBLIC KEY----- ``` ### Signing Flow ``` 1. Build package (ZIP all files) 2. Generate MANIFEST.MF with SHA-256 hashes 3. Sign MANIFEST.MF with private key 4. Store signature in META-INF/CERT.SIG 5. Include public key in META-INF/CERT.PEM ``` #### MANIFEST.MF Example ``` Manifest-Version: 1.0 Created-By: mosis-cli 1.0.0 Package-Id: com.developer.myapp Version-Code: 1 Name: manifest.json SHA-256-Digest: K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols= Name: assets/main.rml SHA-256-Digest: uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek= ``` ### Verification Flow ``` 1. Extract MANIFEST.MF from package 2. Extract CERT.SIG (signature) 3. Extract CERT.PEM (public key) 4. Verify signature of MANIFEST.MF using public key 5. Verify CERT.PEM matches registered developer key 6. Verify each file hash matches MANIFEST.MF entry ``` ### Key Registration ``` Developer Portal: ├── Go to Settings > Signing Keys ├── Click "Add Key" ├── Paste public key (PEM format) ├── Verify fingerprint matches local └── Key is now registered Server stores: ├── public_key (PEM text) ├── fingerprint (SHA256 of public key) ├── created_at └── is_active ``` ### Key Rotation ``` 1. Generate new keypair 2. Register new public key in portal 3. Sign new versions with new key 4. (Optional) Revoke old key after transition period ``` ### Trust Model ``` ┌─────────────────────────────────────────────────┐ │ Trust Chain │ ├─────────────────────────────────────────────────┤ │ │ │ Developer │ │ │ │ │ ▼ │ │ Private Key ──signs──► Package │ │ │ │ │ ▼ │ │ Public Key ──registered──► Portal │ │ │ │ │ ▼ │ │ Portal ──verifies──► Signature │ │ │ │ │ ▼ │ │ Device ──trusts──► Portal-verified packages │ │ │ └─────────────────────────────────────────────────┘ ``` --- ## Security Considerations ### Password Storage (if used) ``` Algorithm: Argon2id Memory: 64 MB Iterations: 3 Parallelism: 4 Salt: 16 bytes random ``` ### Token Security - Access tokens: Short-lived, in-memory only - Refresh tokens: HttpOnly, Secure, SameSite=Strict - API keys: Hashed with bcrypt, shown once on creation ### Key Security - Private keys never leave developer's machine - Public keys verified via fingerprint - Key compromise: Revoke immediately, re-sign apps ### Audit Logging ```sql INSERT INTO auth_audit_log ( developer_id, action, -- login, logout, key_create, key_revoke ip_address, user_agent, success, failure_reason, timestamp ) VALUES (...); ``` --- ## API Endpoints ### Authentication ``` POST /auth/oauth/github # Start GitHub OAuth GET /auth/oauth/github/callback # GitHub callback POST /auth/oauth/google # Start Google OAuth GET /auth/oauth/google/callback # Google callback POST /auth/refresh # Refresh tokens POST /auth/logout # Invalidate tokens GET /auth/me # Get current user ``` ### API Keys ``` GET /api-keys # List keys POST /api-keys # Create key DELETE /api-keys/:id # Revoke key ``` ### Signing Keys ``` GET /signing-keys # List keys POST /signing-keys # Register key DELETE /signing-keys/:id # Revoke key GET /signing-keys/:id/verify # Verify a signature ``` --- ## Implementation (Go) ### Dependencies ```go import ( // OAuth2 "golang.org/x/oauth2" "golang.org/x/oauth2/github" "golang.org/x/oauth2/google" // JWT "github.com/golang-jwt/jwt/v5" // Cryptography (all stdlib) "crypto/ed25519" "crypto/rand" "crypto/sha256" // Password/Key hashing "golang.org/x/crypto/argon2" "golang.org/x/crypto/bcrypt" ) ``` ### OAuth2 Config ```go var githubOAuth = &oauth2.Config{ ClientID: os.Getenv("GITHUB_CLIENT_ID"), ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"), Endpoint: github.Endpoint, Scopes: []string{"read:user", "user:email"}, RedirectURL: "https://portal.mosis.dev/auth/github/callback", } ``` ### JWT Generation ```go func generateAccessToken(developerID string) (string, error) { claims := jwt.MapClaims{ "sub": developerID, "type": "access", "iat": time.Now().Unix(), "exp": time.Now().Add(time.Hour).Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(os.Getenv("JWT_SECRET"))) } ``` ### Ed25519 Signing ```go func signManifest(manifest []byte, privateKey ed25519.PrivateKey) []byte { return ed25519.Sign(privateKey, manifest) } func verifySignature(manifest, signature []byte, publicKey ed25519.PublicKey) bool { return ed25519.Verify(publicKey, manifest, signature) } ``` ### API Key Hashing ```go func hashAPIKey(key string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(key), bcrypt.DefaultCost) return string(hash), err } func verifyAPIKey(key, hash string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(key)) == nil } ``` --- ## Deliverables - [x] Auth approach decided (OAuth2 + JWT + API Keys) - [x] Crypto libraries selected (Go stdlib + golang-jwt) - [ ] OAuth2 integration (GitHub) - P0 - [ ] OAuth2 integration (Google) - P1 - [ ] JWT token management - [ ] API key generation and validation - [ ] Ed25519 key generation (CLI tool) - [ ] Signature creation and verification - [ ] Key registration API - [ ] Audit logging --- ## Test Cases | Test | Description | |------|-------------| | OAuthLogin | Complete OAuth flow | | TokenRefresh | Refresh expired access token | | InvalidToken | Reject tampered JWT | | APIKeyAuth | Authenticate with API key | | KeyGeneration | Generate valid Ed25519 keypair | | SignPackage | Sign and verify package | | InvalidSignature | Reject tampered package | | KeyRevocation | Revoked key fails verification | --- ## Open Questions 1. ~~Support for hardware security keys (YubiKey)?~~ → Defer to post-MVP 2. ~~Multi-factor authentication for portal?~~ → Defer to post-MVP 3. Team accounts with role-based access? → Consider for v1.1 4. ~~Key escrow for enterprise customers?~~ → Not needed for self-hosted --- ## References - [OAuth 2.0 RFC 6749](https://tools.ietf.org/html/rfc6749) - [JWT RFC 7519](https://tools.ietf.org/html/rfc7519) - [Ed25519 paper](https://ed25519.cr.yp.to/ed25519-20110926.pdf) - [OWASP Authentication Cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)