Files
MosisService/docs/DEV_PORTAL_M04_AUTH.md

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

  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

{
  "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

  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