# Part 6: OAuth Provider Architecture

**Previous:** [`docs/05-cli.md`](05-cli.md)

---

## Overview

Packr supports OAuth 2.0 login via a pluggable provider system. Instead of embedding provider-specific code throughout the registry, each provider implements a common `OAuthProvider` interface. This makes it straightforward to add new providers — including custom OIDC providers such as Keycloak, Okta, or Azure AD — without modifying core auth logic.

OAuth login is primarily accessed via the **OAuth device flow** (RFC 8628), which is designed for headless and CI environments where opening a browser is not always possible. The CLI initiates the flow, the user completes it in a browser, and the CLI polls for the result.

---

## OAuthProvider Interface

All providers implement this Go interface:

```go
type OAuthProvider interface {
    // Name returns the provider identifier used in URLs and config.
    // Examples: "github", "google", "gitlab", "oidc"
    Name() string

    // AuthorizeURL builds the provider's authorization URL.
    // state is a random CSRF token generated per-request.
    AuthorizeURL(state string) string

    // ExchangeCode exchanges an authorization code for an access token,
    // then fetches the user's profile from the provider.
    ExchangeCode(code string) (*OAuthUser, error)
}

type OAuthUser struct {
    ProviderID string // Unique ID from the provider
    Email      string // Primary email
    Login      string // Username / handle
    Name       string // Display name
    AvatarURL  string // Profile picture URL
}
```

---

## Built-in Providers

| Provider | Name | Required Env Vars |
|----------|------|-------------------|
| GitHub | `github` | `OAUTH_GITHUB_CLIENT_ID`, `OAUTH_GITHUB_CLIENT_SECRET` |
| Google | `google` | `OAUTH_GOOGLE_CLIENT_ID`, `OAUTH_GOOGLE_CLIENT_SECRET` |
| GitLab | `gitlab` | `OAUTH_GITLAB_CLIENT_ID`, `OAUTH_GITLAB_CLIENT_SECRET`, optional `OAUTH_GITLAB_BASE_URL` |
| Generic OIDC | `oidc` | `OAUTH_OIDC_CLIENT_ID`, `OAUTH_OIDC_CLIENT_SECRET`, `OAUTH_OIDC_ISSUER_URL` |

All providers also require `OAUTH_REDIRECT_BASE` to be set to the public dashboard URL (e.g. `https://packr.blueforge.studio`). The provider redirect URI is built as `{OAUTH_REDIRECT_BASE}/api/auth/callback/{provider}`.

Providers are auto-registered at startup if their required env vars are set. A provider is omitted if its credentials are missing, so you can run with only the providers you need.

---

## Forge-auth Integration

**Forge-auth** is BlueForge Studio's internal SSO service. When enabled, it acts as an OIDC provider that fronts your organization's identity stack (GitHub, Google Workspace, or any SAML IdP). From Packr's perspective, forge-auth is a standard OIDC provider.

### How it works

1. The CLI starts a device flow and gets a `verification_uri` pointing to forge-auth.
2. The user completes SSO in their browser (MFA, organization policy, etc.).
3. Forge-auth issues an OIDC token with the user's email and organization membership claims.
4. Packr exchanges this for its own JWT access token.

### Env Vars

| Variable | Description |
|----------|-------------|
| `FORGE_AUTH_URL` | Base URL of the forge-auth service (e.g. `https://auth.blueforge.studio`) |
| `FORGE_AUTH_CLIENT_ID` | OAuth client ID registered in forge-auth |
| `FORGE_AUTH_CLIENT_SECRET` | OAuth client secret |

---

## Auth Mode: PACKR_AUTH_MODE

Packr supports three auth modes:

| Mode | Description |
|------|-------------|
| `standalone` | Local username/password only. No OAuth providers. |
| `forge-auth` | Forge-auth SSO only. Password login disabled. |
| `hybrid` | Both password login and OAuth providers active. Default. |

Set with:

```bash
PACKR_AUTH_MODE=hybrid  # default
PACKR_AUTH_MODE=standalone
PACKR_AUTH_MODE=forge-auth
```

---

## OAuth Device Flow (RFC 8628)

The device flow allows the CLI (a "device") to initiate OAuth login without a browser. The user completes the flow on a separate device or browser tab.

### Step-by-step

```
CLI                         Registry                    OAuth Provider
 |                              |                              |
 |--POST /-/v1/device/authorize->|                              |
 |  { provider: "github" }      |                              |
 |                              |-- generates device_code,     |
 |                              |   user_code, verification_uri|
 |<-- { device_code,            |                              |
 |      user_code,              |                              |
 |      verification_uri,       |                              |
 |      expires_in: 900 } ------+                              |
 |                              |                              |
 | [CLI prints:]                |                              |
 | "Open: https://registry/verify?code=ABCD-1234"             |
 | [polls every 5s...]          |                              |
 |                              |                              |
 |          [User opens browser, visits verification_uri]      |
 |                              |                              |
 |                              |<-- GET /verify?code=ABCD-1234|
 |                              |-- redirects to OAuth provider|
 |                              |                              |
 |                              |          [User logs in with provider]
 |                              |                              |
 |                              |<-- callback with auth code --+
 |                              |-- ExchangeCode()             |
 |                              |-- creates/updates user       |
 |                              |-- issues JWT                 |
 |                              |-- marks device_code as done  |
 |                              |                              |
 |--POST /-/v1/device/token---->|                              |
 |  { device_code }             |                              |
 |<-- { access_token, token_type: "Bearer" } ----------------+
 |                              |                              |
 | [CLI stores token, done]     |                              |
```

### Polling States

When polling `POST /-/v1/device/token`, the registry returns one of:

| State | Response | Meaning |
|-------|----------|---------|
| Pending | `400 authorization_pending` | User hasn't finished yet. Keep polling. |
| Slow down | `400 slow_down` | Polling too fast. Back off. |
| Expired | `400 expired_token` | Code expired (15 min). Start over. |
| Done | `200 { access_token }` | Success. Store the token. |

---

## Configuring Providers

### GitHub

1. Go to **GitHub Settings → Developer settings → OAuth Apps → New OAuth App**
2. Set **Authorization callback URL** to `https://your-dashboard/api/auth/callback/github`
   (e.g. `https://packr.blueforge.studio/api/auth/callback/github`)
3. Copy **Client ID** and **Client Secret**

```bash
OAUTH_GITHUB_CLIENT_ID=Ov23liABCDEFGH123456
OAUTH_GITHUB_CLIENT_SECRET=abc123def456...
OAUTH_REDIRECT_BASE=https://packr.blueforge.studio
```

### Google

1. Go to **Google Cloud Console → APIs & Services → Credentials → Create OAuth 2.0 Client**
2. Set **Authorized redirect URI** to `https://your-dashboard/api/auth/callback/google`
3. Copy **Client ID** and **Client Secret**

```bash
OAUTH_GOOGLE_CLIENT_ID=123456789-abc.apps.googleusercontent.com
OAUTH_GOOGLE_CLIENT_SECRET=GOCSPX-abc123...
OAUTH_REDIRECT_BASE=https://packr.blueforge.studio
```

### GitLab

1. Go to **GitLab → Preferences → Applications → Add new application**
2. Set **Redirect URI** to `https://your-dashboard/api/auth/callback/gitlab`
3. Select scopes: `read_user`, `email`

```bash
OAUTH_GITLAB_CLIENT_ID=abc123def456...
OAUTH_GITLAB_CLIENT_SECRET=gloas-abc123...
OAUTH_GITLAB_BASE_URL=https://gitlab.com  # or your self-hosted GitLab URL
OAUTH_REDIRECT_BASE=https://packr.blueforge.studio
```

### Generic OIDC

Supports Keycloak, Okta, Azure AD, Authentik, and any RFC 8414-compliant OIDC provider.

```bash
OAUTH_OIDC_CLIENT_ID=packr
OAUTH_OIDC_CLIENT_SECRET=super-secret
OAUTH_OIDC_ISSUER_URL=https://auth.example.com/realms/myrealm
OAUTH_REDIRECT_BASE=https://packr.blueforge.studio
# Packr will auto-discover endpoints from /.well-known/openid-configuration
```

Configure your OIDC provider's redirect URI to `https://your-dashboard/api/auth/callback/oidc`.

#### Keycloak Example

```bash
OAUTH_OIDC_ISSUER_URL=https://keycloak.example.com/realms/my-org
OAUTH_OIDC_CLIENT_ID=packr-registry
OAUTH_OIDC_CLIENT_SECRET=keycloak-client-secret
```

#### Okta Example

```bash
OAUTH_OIDC_ISSUER_URL=https://dev-12345.okta.com/oauth2/default
OAUTH_OIDC_CLIENT_ID=0oa1b2c3d4e5f6g7h8i9
OAUTH_OIDC_CLIENT_SECRET=okta-client-secret
```

#### Azure AD Example

```bash
OAUTH_OIDC_ISSUER_URL=https://login.microsoftonline.com/your-tenant-id/v2.0
OAUTH_OIDC_CLIENT_ID=azure-app-client-id
OAUTH_OIDC_CLIENT_SECRET=azure-client-secret
```

---

## Adding a Custom Provider

Implement the `OAuthProvider` interface:

```go
type MyProvider struct {
    clientID     string
    clientSecret string
}

func (p *MyProvider) Name() string { return "myprovider" }

func (p *MyProvider) AuthorizeURL(state string) string {
    return fmt.Sprintf(
        "https://auth.example.com/oauth/authorize?client_id=%s&state=%s&response_type=code&scope=email+profile",
        p.clientID, state,
    )
}

func (p *MyProvider) ExchangeCode(code string) (*OAuthUser, error) {
    // 1. POST to token endpoint to get access_token
    // 2. GET user profile
    // 3. Return &OAuthUser{...}
}
```

Register it in `internal/oauth/registry.go`:

```go
if clientID := os.Getenv("MYPROVIDER_CLIENT_ID"); clientID != "" {
    providers["myprovider"] = &MyProvider{
        clientID:     clientID,
        clientSecret: os.Getenv("MYPROVIDER_CLIENT_SECRET"),
    }
}
```

---

## Security

### State Parameter

Every authorization request includes a randomly generated `state` parameter (32 bytes, hex-encoded). The registry verifies the state on callback to prevent CSRF attacks. States are single-use and expire with the device code.

### Device Code Expiry

Device codes and user codes expire after **15 minutes**. After expiry, the user must restart the login flow. This limits the window for phishing attacks where an attacker tries to get a user to approve a stale code.

### Rate Limiting

The device authorize endpoint is rate-limited to **5 requests per minute per IP**. This prevents automated enumeration of user codes.

### Single-Use Codes

Once a device code is exchanged for an access token, it is immediately invalidated. Replaying the same device code returns `expired_token`.

### Account Linking

When a user logs in via OAuth for the first time, Packr looks up an existing account by email. If a match is found, the OAuth identity is linked to that account. If no match is found, a new account is created. This means a user with the same email on GitHub and Google will be linked to the same Packr account automatically.

---

---

## Forge-auth Integration Setup

This section describes how to register Packr as an application in forge-auth and configure the BlueForge ecosystem SSO callback.

### 1. Register Packr in forge-auth Admin

1. Log in to the forge-auth admin console (e.g. `https://auth.blueforge.studio/admin`).
2. Navigate to **Applications → New Application**.
3. Set the application name to `Packr Registry`.
4. Set the **Redirect URI / Callback URL** to:
   ```
   https://packr.blueforge.studio/api/auth/callback/forgeauth
   ```
5. Note the generated **Client ID** and **Client Secret**.

### 2. Configure Environment Variables on Fly.io

Set the following secrets on your Fly.io deployment:

```bash
fly secrets set \
  FORGE_AUTH_URL=https://auth.blueforge.studio \
  FORGE_AUTH_CLIENT_ID=<your-client-id> \
  FORGE_AUTH_CLIENT_SECRET=<your-client-secret> \
  PACKR_AUTH_MODE=hybrid
```

| Variable | Description |
|----------|-------------|
| `FORGE_AUTH_URL` | Base URL of the forge-auth service |
| `FORGE_AUTH_CLIENT_ID` | Client ID from forge-auth app registration |
| `FORGE_AUTH_CLIENT_SECRET` | Client secret from forge-auth app registration |
| `PACKR_AUTH_MODE` | Set to `hybrid` to enable both password login and OAuth, or `forge-auth` for SSO-only |

### 3. Callback URL

The dashboard Next.js app handles the OAuth callback at:

```
https://packr.blueforge.studio/api/auth/callback/forgeauth
```

This route (`packages/site/app/api/auth/callback/forgeauth/route.ts`) handles two flows:

- **Web dashboard login** (`state=web`): Exchanges the authorization code for a session token via the registry admin API and sets a `packr_session` cookie, then redirects to `/dashboard`.
- **CLI device flow** (`state=<device_code>`): Posts the code and device code to the registry's device callback endpoint, then redirects to `/cli/auth` with success or error status.

**Next:** [`docs/07-tokens.md`](07-tokens.md) — Token Permission Model and Roles
