Authentication

API authentication methods and security patterns

GoPie supports multiple authentication methods to secure API access. This guide covers JWT token authentication, API key management, and OAuth2/OIDC flows.

Overview

Authentication Methods

  1. JWT Tokens: Primary method for web applications
  2. API Keys: For server-to-server communication
  3. OAuth2/OIDC: Via Zitadel integration
  4. Session Tokens: For browser-based authentication

Security Principles

  • All API requests must be authenticated
  • Tokens have configurable expiration
  • Role-based access control (RBAC)
  • Organization-level isolation

JWT Authentication

Token Structure

{
  "header": {
    "alg": "RS256",
    "typ": "JWT",
    "kid": "key-id-123"
  },
  "payload": {
    "sub": "user_123",
    "email": "[email protected]",
    "org": "org_456",
    "roles": ["developer", "analyst"],
    "permissions": [
      "datasets:read",
      "datasets:write",
      "queries:execute"
    ],
    "iat": 1634567890,
    "exp": 1634571490,
    "iss": "https://auth.gopie.io"
  }
}

Obtaining Tokens

Login Endpoint

POST /api/v1/auth/login
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "secure_password"
}

Response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "user": {
    "id": "user_123",
    "email": "[email protected]",
    "organization_id": "org_456",
    "roles": ["developer", "analyst"]
  }
}

Using Tokens

Authorization Header

GET /api/v1/datasets
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

Token Validation

func ValidateToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(
        tokenString,
        &Claims{},
        func(token *jwt.Token) (interface{}, error) {
            return publicKey, nil
        },
    )
    
    if err != nil {
        return nil, err
    }
    
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    
    return nil, ErrInvalidToken
}

Token Refresh

Refresh Endpoint

POST /api/v1/auth/refresh
Content-Type: application/json

{
  "refresh_token": "eyJhbGciOiJSUzI1NiIs..."
}

Response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Token Expiration

  • Access Token: 1 hour (configurable)
  • Refresh Token: 30 days (configurable)
  • Grace Period: 5 minutes for clock skew

API Key Authentication

Key Management

Creating API Keys

POST /api/v1/api-keys
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/json

{
  "name": "Production API Key",
  "description": "Key for production data pipeline",
  "permissions": [
    "datasets:read",
    "queries:execute"
  ],
  "expires_at": "2024-12-31T23:59:59Z"
}

Response:

{
  "id": "key_789",
  "key": "gp_live_a1b2c3d4e5f6g7h8i9j0",
  "name": "Production API Key",
  "created_at": "2024-01-15T10:00:00Z",
  "expires_at": "2024-12-31T23:59:59Z",
  "permissions": [
    "datasets:read",
    "queries:execute"
  ]
}

Using API Keys

Header Authentication

GET /api/v1/datasets
X-API-Key: gp_live_a1b2c3d4e5f6g7h8i9j0
GET /api/v1/datasets?api_key=gp_live_a1b2c3d4e5f6g7h8i9j0

Key Rotation

POST /api/v1/api-keys/{key_id}/rotate
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

Response:

{
  "old_key": "gp_live_a1b2c3d4e5f6g7h8i9j0",
  "new_key": "gp_live_k1l2m3n4o5p6q7r8s9t0",
  "grace_period_ends": "2024-01-22T10:00:00Z"
}

OAuth2/OIDC Integration

Zitadel Configuration

Discovery Endpoint

https://auth.gopie.io/.well-known/openid-configuration

OAuth2 Flow

Client Configuration

Authorization Request

GET https://auth.gopie.io/oauth/authorize?
  client_id=your_client_id&
  redirect_uri=https://yourapp.com/callback&
  response_type=code&
  scope=openid profile email datasets:read&
  state=random_state_value

Token Exchange

POST https://auth.gopie.io/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=authorization_code_here&
client_id=your_client_id&
client_secret=your_client_secret&
redirect_uri=https://yourapp.com/callback

Scopes and Permissions

Available Scopes

Basic:
  - openid: OpenID Connect support
  - profile: User profile information
  - email: User email address
  - offline_access: Refresh token issuance

Resource Scopes:
  - datasets:read: Read dataset information
  - datasets:write: Create/update datasets
  - datasets:delete: Delete datasets
  - queries:execute: Execute queries
  - organizations:manage: Manage organization

Admin Scopes:
  - admin:users: Manage all users
  - admin:billing: Access billing information
  - admin:system: System administration

Role-Based Access Control

Role Hierarchy

Roles:
  owner:
    inherits: [admin]
    permissions:
      - organizations:delete
      - billing:manage
  
  admin:
    inherits: [developer]
    permissions:
      - organizations:manage
      - users:manage
      - settings:manage
  
  developer:
    inherits: [analyst]
    permissions:
      - datasets:write
      - datasets:delete
      - api-keys:manage
  
  analyst:
    inherits: [viewer]
    permissions:
      - queries:execute
      - datasets:read
      - visualizations:create
  
  viewer:
    permissions:
      - datasets:list
      - queries:read
      - dashboards:view

Permission Checking

func RequirePermission(permission string) fiber.Handler {
    return func(c *fiber.Ctx) error {
        claims := c.Locals("claims").(*Claims)
        
        if !hasPermission(claims, permission) {
            return c.Status(403).JSON(fiber.Map{
                "error": "Insufficient permissions",
                "required": permission,
            })
        }
        
        return c.Next()
    }
}

// Usage
app.Post("/api/v1/datasets", 
    RequireAuth(),
    RequirePermission("datasets:write"),
    datasetHandler.Create,
)

Security Headers

Required Headers

func SecurityHeaders() fiber.Handler {
    return func(c *fiber.Ctx) error {
        c.Set("X-Content-Type-Options", "nosniff")
        c.Set("X-Frame-Options", "DENY")
        c.Set("X-XSS-Protection", "1; mode=block")
        c.Set("Strict-Transport-Security", "max-age=31536000")
        c.Set("Content-Security-Policy", "default-src 'self'")
        return c.Next()
    }
}

CORS Configuration

app.Use(cors.New(cors.Config{
    AllowOrigins: []string{
        "https://app.gopie.io",
        "http://localhost:3000",
    },
    AllowMethods: []string{
        "GET", "POST", "PUT", "DELETE", "OPTIONS",
    },
    AllowHeaders: []string{
        "Origin", "Content-Type", "Accept",
        "Authorization", "X-API-Key",
    },
    AllowCredentials: true,
    MaxAge: 86400,
}))

Error Responses

Authentication Errors

// 401 Unauthorized
{
  "error": "unauthorized",
  "message": "Invalid or missing authentication credentials",
  "code": "AUTH_REQUIRED"
}

// 403 Forbidden
{
  "error": "forbidden",
  "message": "Insufficient permissions for this operation",
  "required_permission": "datasets:write",
  "code": "PERMISSION_DENIED"
}

// Token Expired
{
  "error": "token_expired",
  "message": "The access token has expired",
  "code": "TOKEN_EXPIRED"
}

Best Practices

Security Guidelines

  1. Token Storage

    • Never store tokens in localStorage
    • Use httpOnly cookies for web apps
    • Implement secure token storage for mobile
  2. API Key Management

    • Rotate keys regularly
    • Use different keys per environment
    • Monitor key usage
  3. Permission Design

    • Follow principle of least privilege
    • Regular permission audits
    • Document permission requirements

Implementation Tips

// TypeScript API client with auth
class GoPieClient {
  private token: string | null = null;
  
  async authenticate(email: string, password: string) {
    const response = await fetch('/api/v1/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });
    
    const data = await response.json();
    this.token = data.access_token;
    
    // Set up automatic refresh
    this.scheduleTokenRefresh(data.expires_in);
  }
  
  async request(path: string, options: RequestInit = {}) {
    const headers = {
      ...options.headers,
      'Authorization': `Bearer ${this.token}`,
    };
    
    const response = await fetch(path, { ...options, headers });
    
    if (response.status === 401) {
      await this.refreshToken();
      // Retry request
      return fetch(path, { ...options, headers });
    }
    
    return response;
  }
}

Testing Authentication

Test Credentials

# Development environment only
test_users:
  admin:
    email: [email protected]
    password: test_admin_123
    roles: [admin]
  
  developer:
    email: [email protected]
    password: test_dev_123
    roles: [developer]
  
  analyst:
    email: [email protected]
    password: test_analyst_123
    roles: [analyst]

cURL Examples

# Login
curl -X POST https://api.gopie.io/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"password"}'

# Use token
curl https://api.gopie.io/api/v1/datasets \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."

# API key
curl https://api.gopie.io/api/v1/datasets \
  -H "X-API-Key: gp_live_a1b2c3d4e5f6g7h8i9j0"

Next Steps