12 KiB
12 KiB
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
- Go stdlib crypto - Ed25519 built into Go, no external deps
- Simple JWT - golang-jwt is battle-tested, minimal
- Stateless tokens - No token store needed (SQLite handles refresh token revocation)
- OAuth-first - GitHub OAuth for most developers, minimal password handling
Overview
Authentication covers two areas:
- Developer authentication - Login to portal, API access
- 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
{
"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
-- 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
{
"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
# 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
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
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
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
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
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
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
- Auth approach decided (OAuth2 + JWT + API Keys)
- 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
Support for hardware security keys (YubiKey)?→ Defer to post-MVPMulti-factor authentication for portal?→ Defer to post-MVP- Team accounts with role-based access? → Consider for v1.1
Key escrow for enterprise customers?→ Not needed for self-hosted