Skip to content

Latest commit

 

History

History
230 lines (186 loc) · 5.78 KB

File metadata and controls

230 lines (186 loc) · 5.78 KB
title Implement API Authentication
id api-authentication
skillLevel intermediate
applicationPatternId building-apis
summary Add JWT or session-based authentication to protect your API endpoints.
tags
building-apis
authentication
jwt
security
rule
description
Use middleware to validate authentication tokens before handling requests.
author PaulJPhilp
related
api-middleware
handle-api-errors
lessonOrder 5

Guideline

Implement authentication as middleware that validates tokens and provides user context to route handlers.


Rationale

Authentication protects your API:

  1. Identity verification - Know who's making requests
  2. Access control - Limit what users can do
  3. Audit trail - Track who did what
  4. Rate limiting - Per-user limits

Good Example

import { Effect, Context, Layer, Data } from "effect"
import { HttpServer, HttpServerRequest, HttpServerResponse } from "@effect/platform"

// ============================================
// 1. Define authentication types
// ============================================

interface User {
  readonly id: string
  readonly email: string
  readonly roles: ReadonlyArray<string>
}

class AuthenticatedUser extends Context.Tag("AuthenticatedUser")<
  AuthenticatedUser,
  User
>() {}

class UnauthorizedError extends Data.TaggedError("UnauthorizedError")<{
  readonly reason: string
}> {}

class ForbiddenError extends Data.TaggedError("ForbiddenError")<{
  readonly requiredRole: string
}> {}

// ============================================
// 2. JWT validation service
// ============================================

interface JwtService {
  readonly verify: (token: string) => Effect.Effect<User, UnauthorizedError>
}

class Jwt extends Context.Tag("Jwt")<Jwt, JwtService>() {}

const JwtLive = Layer.succeed(Jwt, {
  verify: (token) =>
    Effect.gen(function* () {
      // In production: use a real JWT library
      if (!token || token === "invalid") {
        return yield* Effect.fail(new UnauthorizedError({ 
          reason: "Invalid or expired token" 
        }))
      }

      // Decode token (simplified)
      if (token.startsWith("user-")) {
        return {
          id: token.replace("user-", ""),
          email: "user@example.com",
          roles: ["user"],
        }
      }

      if (token.startsWith("admin-")) {
        return {
          id: token.replace("admin-", ""),
          email: "admin@example.com",
          roles: ["user", "admin"],
        }
      }

      return yield* Effect.fail(new UnauthorizedError({ 
        reason: "Malformed token" 
      }))
    }),
})

// ============================================
// 3. Authentication middleware
// ============================================

const extractBearerToken = (header: string | undefined): string | null => {
  if (!header?.startsWith("Bearer ")) return null
  return header.slice(7)
}

const authenticate = <A, E, R>(
  handler: Effect.Effect<A, E, R | AuthenticatedUser>
): Effect.Effect<A, E | UnauthorizedError, R | Jwt | HttpServerRequest.HttpServerRequest> =>
  Effect.gen(function* () {
    const request = yield* HttpServerRequest.HttpServerRequest
    const jwt = yield* Jwt

    const authHeader = request.headers["authorization"]
    const token = extractBearerToken(authHeader)

    if (!token) {
      return yield* Effect.fail(new UnauthorizedError({ 
        reason: "Missing Authorization header" 
      }))
    }

    const user = yield* jwt.verify(token)

    return yield* handler.pipe(
      Effect.provideService(AuthenticatedUser, user)
    )
  })

// ============================================
// 4. Role-based authorization
// ============================================

const requireRole = (role: string) =>
  <A, E, R>(handler: Effect.Effect<A, E, R | AuthenticatedUser>) =>
    Effect.gen(function* () {
      const user = yield* AuthenticatedUser

      if (!user.roles.includes(role)) {
        return yield* Effect.fail(new ForbiddenError({ requiredRole: role }))
      }

      return yield* handler
    })

// ============================================
// 5. Protected routes
// ============================================

const getProfile = authenticate(
  Effect.gen(function* () {
    const user = yield* AuthenticatedUser
    return HttpServerResponse.json({
      id: user.id,
      email: user.email,
      roles: user.roles,
    })
  })
)

const adminOnly = authenticate(
  requireRole("admin")(
    Effect.gen(function* () {
      const user = yield* AuthenticatedUser
      return HttpServerResponse.json({
        message: `Welcome admin ${user.email}`,
        users: ["user1", "user2", "user3"],
      })
    })
  )
)

// ============================================
// 6. Error handling
// ============================================

const handleAuthErrors = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
  effect.pipe(
    Effect.catchTag("UnauthorizedError", (e) =>
      Effect.succeed(
        HttpServerResponse.json({ error: e.reason }, { status: 401 })
      )
    ),
    Effect.catchTag("ForbiddenError", (e) =>
      Effect.succeed(
        HttpServerResponse.json(
          { error: `Requires role: ${e.requiredRole}` },
          { status: 403 }
        )
      )
    )
  )

Authentication Flow

Request → Extract Token → Verify JWT → Provide User → Handler
                ↓              ↓
              401           401/403

Token Types

Type Storage Best For
JWT Header Stateless APIs
Session Cookie Web apps
API Key Header Service-to-service

Best Practices

  1. Use HTTPS - Tokens in plain HTTP are visible
  2. Short expiry - JWTs should expire quickly
  3. Refresh tokens - For long sessions
  4. Validate claims - Check issuer, audience, expiry
  5. Log auth events - Track login attempts