| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { Effect, Redacted } from "effect" |
| 3 | import * as Base64 from "effect/encoding/Base64" |
| 4 | import { HttpClientRequest, HttpServerRequest } from "effect/http" |
| 5 | import { HttpApiBuilder, HttpApiSecurity } from "effect/http-api" |
| 6 | |
| 7 | const decode = <Security extends HttpApiSecurity.HttpApiSecurity>(authorization: string, security: Security) => |
| 8 | HttpApiBuilder.securityDecode(security).pipe( |
| 9 | Effect.provideService( |
| 10 | HttpServerRequest.HttpServerRequest, |
| 11 | HttpServerRequest.fromWeb(new Request("http://localhost/", { headers: { authorization } })) |
| 12 | ), |
| 13 | Effect.provideService(HttpServerRequest.ParsedSearchParams, {}) |
| 14 | ) |
| 15 | |
| 16 | describe("HttpApiSecurity", () => { |
| 17 | describe("securityDecode", () => { |
| 18 | it.effect("decodes a bearer token without a leading space", () => |
| 19 | Effect.gen(function*() { |
| 20 | const token = "abc123" |
| 21 | const { headers } = HttpClientRequest.get("http://localhost/").pipe( |
| 22 | HttpClientRequest.bearerToken(token) |
| 23 | ) |
| 24 | const credential = yield* HttpApiBuilder.securityDecode(HttpApiSecurity.bearer).pipe( |
| 25 | Effect.provideService( |
| 26 | HttpServerRequest.HttpServerRequest, |
| 27 | HttpServerRequest.fromWeb(new Request("http://localhost/", { headers })) |
| 28 | ), |
| 29 | Effect.provideService(HttpServerRequest.ParsedSearchParams, {}) |
| 30 | ) |
| 31 | |
| 32 | assert.strictEqual(Redacted.value(credential), token) |
| 33 | })) |
| 34 | |
| 35 | it.effect("decodes a custom http scheme without a leading space", () => |
| 36 | Effect.gen(function*() { |
| 37 | const credential = yield* decode("Token abc123", HttpApiSecurity.http({ scheme: "Token" })) |
| 38 | |
| 39 | assert.strictEqual(Redacted.value(credential), "abc123") |
| 40 | })) |
| 41 | |
| 42 | it.effect("matches HTTP schemes case-insensitively", () => |
| 43 | Effect.gen(function*() { |
| 44 | const credential = yield* decode("bEaReR abc123", HttpApiSecurity.bearer) |
| 45 | |
| 46 | assert.strictEqual(Redacted.value(credential), "abc123") |
| 47 | })) |
| 48 | |
| 49 | it.effect("accepts multiple spaces before HTTP credentials", () => |
| 50 | Effect.gen(function*() { |
| 51 | const credential = yield* decode("Bearer abc123", HttpApiSecurity.bearer) |
| 52 | |
| 53 | assert.strictEqual(Redacted.value(credential), "abc123") |
| 54 | })) |
| 55 | |
| 56 | it.effect("rejects mismatched and malformed HTTP schemes", () => |
| 57 | Effect.gen(function*() { |
| 58 | const mismatched = yield* decode("Basic abc123", HttpApiSecurity.bearer) |
| 59 | const malformed = yield* decode("Bearerabc123", HttpApiSecurity.bearer) |
| 60 | |
| 61 | assert.strictEqual(Redacted.value(mismatched), "") |
| 62 | assert.strictEqual(Redacted.value(malformed), "") |
| 63 | })) |
| 64 | |
| 65 | it.effect("decodes Basic credentials using the first colon separator", () => |
| 66 | Effect.gen(function*() { |
| 67 | const encoded = Base64.encode("alice:secret:with:colons") |
| 68 | const credential = yield* decode(`Basic ${encoded}`, HttpApiSecurity.basic) |
| 69 | |
| 70 | assert.strictEqual(credential.username, "alice") |
| 71 | assert.strictEqual(Redacted.value(credential.password), "secret:with:colons") |
| 72 | })) |
| 73 | |
| 74 | it.effect("rejects Basic credentials from a different scheme", () => |
| 75 | Effect.gen(function*() { |
| 76 | const encoded = Base64.encode("alice:secret") |
| 77 | const credential = yield* decode(`Bearer ${encoded}`, HttpApiSecurity.basic) |
| 78 | |
| 79 | assert.strictEqual(credential.username, "") |
| 80 | assert.strictEqual(Redacted.value(credential.password), "") |
| 81 | })) |
| 82 | }) |
| 83 | }) |