2026-01-29 19:48:36 +09:00
|
|
|
import { describe, expect, it } from "bun:test"
|
|
|
|
|
import { isStepUpRequired, mergeScopes, parseWwwAuthenticate } from "./step-up"
|
|
|
|
|
|
|
|
|
|
describe("parseWwwAuthenticate", () => {
|
|
|
|
|
it("parses scope from simple Bearer header", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const header = 'Bearer scope="read write"'
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = parseWwwAuthenticate(header)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toEqual({ requiredScopes: ["read", "write"] })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("parses scope with error fields", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const header = 'Bearer error="insufficient_scope", scope="admin"'
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = parseWwwAuthenticate(header)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toEqual({
|
|
|
|
|
requiredScopes: ["admin"],
|
|
|
|
|
error: "insufficient_scope",
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("parses all fields including error_description", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const header =
|
|
|
|
|
'Bearer realm="example", error="insufficient_scope", error_description="Need admin access", scope="admin write"'
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = parseWwwAuthenticate(header)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toEqual({
|
|
|
|
|
requiredScopes: ["admin", "write"],
|
|
|
|
|
error: "insufficient_scope",
|
|
|
|
|
errorDescription: "Need admin access",
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns null for non-Bearer scheme", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const header = 'Basic realm="example"'
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = parseWwwAuthenticate(header)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns null when no scope parameter present", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const header = 'Bearer error="invalid_token"'
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = parseWwwAuthenticate(header)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns null for empty scope value", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const header = 'Bearer scope=""'
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = parseWwwAuthenticate(header)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns null for bare Bearer with no params", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const header = "Bearer"
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = parseWwwAuthenticate(header)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("handles case-insensitive Bearer prefix", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const header = 'bearer scope="read"'
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = parseWwwAuthenticate(header)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toEqual({ requiredScopes: ["read"] })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("parses single scope value", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const header = 'Bearer scope="admin"'
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = parseWwwAuthenticate(header)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toEqual({ requiredScopes: ["admin"] })
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("mergeScopes", () => {
|
|
|
|
|
it("merges new scopes into existing", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const existing = ["read", "write"]
|
|
|
|
|
const required = ["admin", "write"]
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = mergeScopes(existing, required)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toEqual(["read", "write", "admin"])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns required when existing is empty", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const existing: string[] = []
|
|
|
|
|
const required = ["read", "write"]
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = mergeScopes(existing, required)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toEqual(["read", "write"])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns existing when required is empty", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const existing = ["read"]
|
|
|
|
|
const required: string[] = []
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = mergeScopes(existing, required)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toEqual(["read"])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("deduplicates identical scopes", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const existing = ["read", "write"]
|
|
|
|
|
const required = ["read", "write"]
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = mergeScopes(existing, required)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toEqual(["read", "write"])
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("isStepUpRequired", () => {
|
|
|
|
|
it("returns step-up info for 403 with WWW-Authenticate", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const statusCode = 403
|
|
|
|
|
const headers = { "www-authenticate": 'Bearer scope="admin"' }
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = isStepUpRequired(statusCode, headers)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toEqual({ requiredScopes: ["admin"] })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns null for non-403 status", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const statusCode = 401
|
|
|
|
|
const headers = { "www-authenticate": 'Bearer scope="admin"' }
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = isStepUpRequired(statusCode, headers)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns null when no WWW-Authenticate header", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const statusCode = 403
|
|
|
|
|
const headers = { "content-type": "application/json" }
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = isStepUpRequired(statusCode, headers)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("handles capitalized WWW-Authenticate header", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const statusCode = 403
|
|
|
|
|
const headers = { "WWW-Authenticate": 'Bearer scope="read write"' }
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = isStepUpRequired(statusCode, headers)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toEqual({ requiredScopes: ["read", "write"] })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns null for 403 with unparseable WWW-Authenticate", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-29 19:48:36 +09:00
|
|
|
const statusCode = 403
|
|
|
|
|
const headers = { "www-authenticate": 'Basic realm="example"' }
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = isStepUpRequired(statusCode, headers)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
})
|