Skip to main content
The auth package provides authentication and auditing capabilities for secure Go applications. It includes OpenID Connect (OIDC) authentication with PKCE support and comprehensive audit logging.

Sub-packages

OIDC

OpenID Connect authentication with PKCE flow

Audit

Audit logging for tracking user actions and system events

Installation

go get github.com/raystack/salt/auth/oidc
go get github.com/raystack/salt/auth/audit

OIDC Package

The oidc package provides OpenID Connect authentication with PKCE (Proof Key for Code Exchange) support for enhanced security.

Features

  • PKCE Support: Implements RFC 7636 for secure authorization
  • Browser-based Flow: Automatic browser opening for user authentication
  • Token Management: Handles access tokens, refresh tokens, and ID tokens
  • OAuth2 Integration: Built on top of golang.org/x/oauth2

Creating a Token Source

func NewTokenSource(ctx context.Context, conf *oauth2.Config, audience string) oauth2.TokenSource
Creates a new token source that handles the OIDC authentication flow. Example:
package main

import (
    "context"
    "fmt"
    "log"
    
    "github.com/raystack/salt/auth/oidc"
    "golang.org/x/oauth2"
)

func main() {
    ctx := context.Background()
    
    config := &oauth2.Config{
        ClientID:     "your-client-id",
        ClientSecret: "your-client-secret",
        RedirectURL:  "http://localhost:8080/callback",
        Endpoint: oauth2.Endpoint{
            AuthURL:  "https://auth.example.com/authorize",
            TokenURL: "https://auth.example.com/token",
        },
        Scopes: []string{"profile", "email"},
    }
    
    tokenSource := oidc.NewTokenSource(ctx, config, "your-api-audience")
    
    token, err := tokenSource.Token()
    if err != nil {
        log.Fatal("Failed to get token:", err)
    }
    
    fmt.Printf("Access Token: %s\n", token.AccessToken)
}

PKCE Flow

The OIDC package automatically implements the PKCE flow:
  1. Generate Code Verifier: Random 32-byte string
  2. Create Code Challenge: SHA256 hash of verifier
  3. Authorization Request: Includes code challenge
  4. Token Exchange: Includes code verifier
This prevents authorization code interception attacks.

Google Service Account Support

The package also supports Google Service Account authentication:
package main

import (
    "context"
    "log"
    
    "github.com/raystack/salt/auth/oidc"
    "google.golang.org/api/option"
)

func main() {
    ctx := context.Background()
    
    // Use service account credentials
    tokenSource, err := oidc.NewGoogleServiceAccountTokenSource(
        ctx,
        "path/to/service-account.json",
        "https://api.example.com",
    )
    if err != nil {
        log.Fatal(err)
    }
    
    token, err := tokenSource.Token()
    if err != nil {
        log.Fatal(err)
    }
    
    // Use token for API calls
    _ = token
}

Cobra Integration

The package provides helpers for integrating OIDC authentication into Cobra CLI applications:
package main

import (
    "github.com/raystack/salt/auth/oidc"
    "github.com/spf13/cobra"
)

func main() {
    var clientID, clientSecret string
    
    loginCmd := &cobra.Command{
        Use:   "login",
        Short: "Authenticate with OIDC",
        RunE: func(cmd *cobra.Command, args []string) error {
            return oidc.LoginCommand(cmd.Context(), clientID, clientSecret)
        },
    }
    
    loginCmd.Flags().StringVar(&clientID, "client-id", "", "OAuth2 client ID")
    loginCmd.Flags().StringVar(&clientSecret, "client-secret", "", "OAuth2 client secret")
    
    rootCmd := &cobra.Command{Use: "app"}
    rootCmd.AddCommand(loginCmd)
    rootCmd.Execute()
}

Audit Package

The audit package provides comprehensive audit logging for tracking user actions and system events.

Features

  • Actor Tracking: Records who performed each action
  • Metadata Support: Attach custom metadata to audit logs
  • Repository Pattern: Pluggable storage backends
  • Context Integration: Extracts audit information from context

Core Types

Service

type Service struct {
    // Internal fields
}
The main audit service that handles logging.

Log

type Log struct {
    Timestamp time.Time              `json:"timestamp"`
    Actor     string                 `json:"actor"`
    Action    string                 `json:"action"`
    Data      interface{}            `json:"data"`
    Metadata  map[string]interface{} `json:"metadata"`
}
Represents a single audit log entry.

Creating an Audit Service

func New(opts ...AuditOption) *Service
Example:
package main

import (
    "context"
    "log"
    
    "github.com/raystack/salt/auth/audit"
    "github.com/raystack/salt/auth/audit/repositories"
)

func main() {
    // Create PostgreSQL repository
    repo, err := repositories.NewPostgres(dbConfig)
    if err != nil {
        log.Fatal(err)
    }
    
    // Create audit service
    auditSvc := audit.New(
        audit.WithRepository(repo),
        audit.WithActorExtractor(extractActorFromContext),
        audit.WithMetadataExtractor(extractMetadataFromContext),
    )
    
    // Log an action
    ctx := audit.WithActor(context.Background(), "[email protected]")
    err = auditSvc.Log(ctx, "user.login", map[string]interface{}{
        "ip_address": "192.168.1.1",
        "user_agent": "Mozilla/5.0",
    })
    if err != nil {
        log.Fatal(err)
    }
}

Audit Options

WithRepository

func WithRepository(r repository) AuditOption
Sets the storage repository for audit logs.
repo, _ := repositories.NewPostgres(cfg)
auditSvc := audit.New(audit.WithRepository(repo))

WithActorExtractor

func WithActorExtractor(fn func(context.Context) (string, error)) AuditOption
Customizes how the actor (user) is extracted from context.
extractActor := func(ctx context.Context) (string, error) {
    user, ok := ctx.Value("user").(string)
    if !ok {
        return "system", nil
    }
    return user, nil
}

auditSvc := audit.New(audit.WithActorExtractor(extractActor))

WithMetadataExtractor

func WithMetadataExtractor(fn func(context.Context) map[string]interface{}) AuditOption
Extracts additional metadata from context.
extractMetadata := func(ctx context.Context) map[string]interface{} {
    return map[string]interface{}{
        "request_id": ctx.Value("request_id"),
        "trace_id":   ctx.Value("trace_id"),
    }
}

auditSvc := audit.New(audit.WithMetadataExtractor(extractMetadata))

Context Helpers

WithActor

func WithActor(ctx context.Context, actor string) context.Context
Adds actor information to context.
ctx := audit.WithActor(ctx, "[email protected]")

WithMetadata

func WithMetadata(ctx context.Context, md map[string]interface{}) (context.Context, error)
Adds metadata to context.
ctx, err := audit.WithMetadata(ctx, map[string]interface{}{
    "ip_address": "192.168.1.1",
    "user_agent": "Mozilla/5.0",
})

Logging Actions

func (s *Service) Log(ctx context.Context, action string, data interface{}) error
Logs an audit event. Example:
// User login
ctx := audit.WithActor(ctx, "[email protected]")
err := auditSvc.Log(ctx, "user.login", map[string]interface{}{
    "method": "password",
    "success": true,
})

// Resource creation
err = auditSvc.Log(ctx, "resource.create", map[string]interface{}{
    "resource_type": "project",
    "resource_id":   "proj-123",
    "name":          "My Project",
})

// Permission change
err = auditSvc.Log(ctx, "permission.grant", map[string]interface{}{
    "user_id":    "user-456",
    "resource_id": "proj-123",
    "permission":  "admin",
})

PostgreSQL Repository

The package includes a PostgreSQL repository implementation:
package main

import (
    "github.com/raystack/salt/auth/audit/repositories"
    "github.com/raystack/salt/db"
)

func main() {
    dbClient, _ := db.New(db.Config{
        Driver: "postgres",
        URL:    "postgres://localhost:5432/mydb",
    })
    
    repo := repositories.NewPostgres(dbClient)
    
    auditSvc := audit.New(audit.WithRepository(repo))
}

Complete Example: Authenticated API with Audit

package main

import (
    "context"
    "log"
    "net/http"
    
    "github.com/raystack/salt/auth/audit"
    "github.com/raystack/salt/auth/audit/repositories"
    "github.com/raystack/salt/auth/oidc"
    "github.com/raystack/salt/db"
    "golang.org/x/oauth2"
)

func main() {
    // Setup audit logging
    dbClient, _ := db.New(db.Config{
        Driver: "postgres",
        URL:    "postgres://localhost:5432/audit",
    })
    
    repo := repositories.NewPostgres(dbClient)
    auditSvc := audit.New(
        audit.WithRepository(repo),
        audit.WithActorExtractor(func(ctx context.Context) (string, error) {
            user, _ := ctx.Value("user").(string)
            return user, nil
        }),
    )
    
    // Setup OIDC
    oauthConfig := &oauth2.Config{
        ClientID:     "client-id",
        ClientSecret: "client-secret",
        RedirectURL:  "http://localhost:8080/callback",
        Endpoint: oauth2.Endpoint{
            AuthURL:  "https://auth.example.com/authorize",
            TokenURL: "https://auth.example.com/token",
        },
    }
    
    // Authentication middleware
    authMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
            token := r.Header.Get("Authorization")
            if token == "" {
                http.Error(w, "Unauthorized", http.StatusUnauthorized)
                return
            }
            
            // Validate token and extract user
            user := "[email protected]" // Extract from token
            ctx := context.WithValue(r.Context(), "user", user)
            
            next(w, r.WithContext(ctx))
        }
    }
    
    // API endpoint with audit logging
    http.HandleFunc("/api/resource", authMiddleware(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        user := ctx.Value("user").(string)
        
        // Log the action
        auditCtx := audit.WithActor(ctx, user)
        auditSvc.Log(auditCtx, "resource.access", map[string]interface{}{
            "method": r.Method,
            "path":   r.URL.Path,
        })
        
        w.Write([]byte("Resource data"))
    }))
    
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Best Practices

Log all actions that involve data access, modification, or permission changes:
auditSvc.Log(ctx, "data.export", exportDetails)
Attach meaningful metadata to audit logs:
ctx, _ = audit.WithMetadata(ctx, map[string]interface{}{
    "ip_address": clientIP,
    "user_agent": userAgent,
})
Follow a naming convention for actions:
// Format: <resource>.<action>
"user.login"
"project.create"
"permission.revoke"
Never log tokens or passwords in audit logs:
auditSvc.Log(ctx, "auth.success", map[string]interface{}{
    "method": "password",
    // Don't include actual password or token
})

Build docs developers (and LLMs) love