Files

123 lines
4.5 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import { Buffer } from "node:buffer"
import { createPrivateKey, sign } from "node:crypto"
import {
buildConnectDeepLink,
buildConnectExchangeDeepLink,
CONNECT_LINK_AUDIENCE,
type ConnectLinkClaims,
} from "../src/index"
import {
generateConnectLinkKeyPair,
signConnectLinkToken,
verifyConnectLinkToken,
} from "../src/node"
const NOW = 1_783_000_000
const KID = "owc-test"
const { publicKeyPem, privateKeyPem } = generateConnectLinkKeyPair()
const publicKeys = { [KID]: publicKeyPem }
function claims(overrides: Partial<ConnectLinkClaims> = {}): ConnectLinkClaims {
return {
iss: "https://api.openwork.acme.example.com",
aud: CONNECT_LINK_AUDIENCE,
iat: NOW,
exp: NOW + 72 * 3600,
jti: "test-jti-0001",
v: 1,
org: { name: "Acme Robotics" },
brand: { appName: "Acme Work", logoUrl: null, iconUrl: null },
den: {
baseUrl: "https://openwork.acme.example.com",
apiBaseUrl: "https://api.openwork.acme.example.com",
},
requireSignin: true,
...overrides,
}
}
function mintRaw(header: object, payload: object) {
const encode = (value: object) => Buffer.from(JSON.stringify(value), "utf8").toString("base64url")
const signingInput = `${encode(header)}.${encode(payload)}`
const signature = sign(null, Buffer.from(signingInput, "utf8"), createPrivateKey(privateKeyPem))
return `${signingInput}.${signature.toString("base64url")}`
}
function verifyAt(token: string, nowEpochSeconds = NOW) {
return verifyConnectLinkToken({ token, publicKeys, nowEpochSeconds })
}
describe("signed desktop connect links", () => {
test("round-trips exact organization configuration", () => {
const expected = claims()
const token = signConnectLinkToken({ claims: expected, privateKeyPem, kid: KID })
const result = verifyAt(token)
expect(result.ok).toBe(true)
if (!result.ok) throw new Error("expected valid token")
expect(result.claims).toEqual(expected)
expect(result.kid).toBe(KID)
expect(result.transport).toBe("signed")
})
test("rejects tampered payloads", () => {
const token = signConnectLinkToken({ claims: claims(), privateKeyPem, kid: KID })
const [header, , signature] = token.split(".")
const forged = Buffer.from(JSON.stringify(claims({
den: { baseUrl: "https://evil.example.com" },
}))).toString("base64url")
const result = verifyAt(`${header}.${forged}.${signature}`)
expect(result.ok).toBe(false)
if (!result.ok) expect(result.code).toBe("bad_signature")
})
test("rejects algorithm confusion, critical headers, and unknown keys", () => {
const tokens = [
mintRaw({ alg: "none", kid: KID }, claims()),
mintRaw({ alg: "HS256", typ: "JWT", kid: KID }, claims()),
mintRaw({ alg: "EdDSA", typ: "JWT", kid: KID, crit: ["exp"] }, claims()),
mintRaw({ alg: "EdDSA", typ: "JWT", kid: "other" }, claims()),
]
for (const token of tokens) {
expect(verifyAt(token).ok).toBe(false)
}
})
test("enforces expiry and issued-at with clock skew", () => {
const token = signConnectLinkToken({ claims: claims(), privateKeyPem, kid: KID })
expect(verifyAt(token, NOW + 72 * 3600 + 30).ok).toBe(true)
const expired = verifyAt(token, NOW + 72 * 3600 + 61)
expect(expired.ok).toBe(false)
if (!expired.ok) expect(expired.code).toBe("expired")
const future = verifyAt(token, NOW - 61)
expect(future.ok).toBe(false)
if (!future.ok) expect(future.code).toBe("not_yet_valid")
})
test("refuses non-HTTPS organization servers except explicit dev loopback", () => {
const insecure = claims({ den: { baseUrl: "http://intranet.example.com" } })
expect(() => signConnectLinkToken({ claims: insecure, privateKeyPem, kid: KID })).toThrow(/non-https/)
const loopback = claims({ den: { baseUrl: "http://127.0.0.1:8790" } })
const token = signConnectLinkToken({
claims: loopback,
privateKeyPem,
kid: KID,
allowInsecureUrls: true,
})
expect(verifyAt(token).ok).toBe(false)
expect(verifyConnectLinkToken({
token,
publicKeys,
nowEpochSeconds: NOW,
allowInsecureLoopback: true,
}).ok).toBe(true)
})
test("builds only the dedicated connect route", () => {
expect(buildConnectDeepLink("abc.def.ghi")).toBe("openwork://connect?token=abc.def.ghi")
expect(buildConnectExchangeDeepLink("abcdefghijklmnopqrstuvwxyz123456", "https://den.example.com/api/den"))
.toBe("openwork://connect?code=abcdefghijklmnopqrstuvwxyz123456&apiBaseUrl=https%3A%2F%2Fden.example.com%2Fapi%2Fden")
})
})