diff --git a/packages/core/src/rp-login/didcomm.ts b/packages/core/src/rp-login/didcomm.ts index 957a5bf..2750d36 100644 --- a/packages/core/src/rp-login/didcomm.ts +++ b/packages/core/src/rp-login/didcomm.ts @@ -18,8 +18,17 @@ import { packAuthcrypt, packAuthcryptJson, wrapForward, type Identity } from ".. import type { RemoteDidcommEndpoint } from "../vta/didcomm.js"; import type { DidcommMessageBridge } from "../vta/transport.js"; -const MSG_AUTHENTICATE = "https://affinidi.com/webvh/1.0/authenticate"; -const MSG_AUTH_RESPONSE = "https://affinidi.com/webvh/1.0/authenticate-response"; +// The canonical Trust-Task auth URIs, matching `did-hosting-common`'s +// `MSG_AUTHENTICATE` / `MSG_AUTH_RESPONSE` verbatim. This package sent the +// retired `affinidi.com/webvh/1.0/*` pair, which the control plane's DIDComm +// router no longer binds — so the request did not route, and a reply under the +// canonical type would have been rejected here as the wrong type anyway. Login +// over DIDComm could not succeed against a current RP at all. +// +// Matched with `===` against the spelling the RP declares today; no +// both-spellings fold, per this repo's rule on compatibility arms. +const MSG_AUTHENTICATE = "https://trusttasks.org/spec/auth/authenticate/0.1"; +const MSG_AUTH_RESPONSE = "https://trusttasks.org/spec/auth/authenticate/0.1#response"; const DEFAULT_TIMEOUT_MS = 30_000; @@ -64,8 +73,19 @@ export async function loginViaDidcomm(opts: DidcommLoginOptions): Promise { + const b = bridge({ + from: RP_DID, + type: AUTH_RESPONSE, + body: { + session_id: "s1", + access_token: "at", + refresh_token: "rt", + access_expires_at: 111, + refresh_expires_at: 222, + }, + }); + + const out = await loginViaDidcomm(opts(b)); + assert.equal(out.sessionId, "s1"); + assert.equal(out.accessToken, "at"); + assert.equal(out.refreshToken, "rt"); + assert.equal(out.accessExpiresAt, 111); + assert.equal(out.refreshExpiresAt, 222); +}); + +test("the retired response type is refused — no both-spellings fold", async () => { + const b = bridge({ + from: RP_DID, + type: "https://affinidi.com/webvh/1.0/authenticate-response", + body: { session_id: "s", access_token: "a", refresh_token: "r" }, + }); + await assert.rejects( + () => loginViaDidcomm(opts(b)), + /authenticate-response/, + "the legacy type must not be accepted alongside the canonical one", + ); +}); + +test("a reply from someone other than the RP is refused", async () => { + const b = bridge({ + from: "did:web:imposter.example", + type: AUTH_RESPONSE, + body: { session_id: "s", access_token: "a", refresh_token: "r" }, + }); + await assert.rejects(() => loginViaDidcomm(opts(b)), /!= RP/); +}); + +test("a response missing a token is refused rather than half-returned", async () => { + const b = bridge({ from: RP_DID, type: AUTH_RESPONSE, body: { session_id: "s" } }); + await assert.rejects(() => loginViaDidcomm(opts(b)), /malformed/); +}); + +test("the request goes out under the canonical authenticate type", async () => { + // The assertion that would have caught the drift. Everything above tests the + // reply; the request type is what the RP's router binds, and sending the + // retired one meant the message never reached a handler at all. + const b = bridge({ + from: RP_DID, + type: AUTH_RESPONSE, + body: { session_id: "s", access_token: "a", refresh_token: "r" }, + }); + await loginViaDidcomm(opts(b)); + + // Unpack as the RP would: the authcrypt recipient, with the holder as the + // verified sender. + const opened = await unpack( + b.calls[0].packed, + { kid: rpParty.kid, privateJwk: rpParty.secretJwk }, + { publicJwk: holderParty.publicJwk }, + ); + assert.equal(opened.message.type, AUTHENTICATE); + assert.equal(opened.message.from, holderParty.did); + assert.deepEqual(opened.message.to, [RP_DID]); + // Empty by contract: the RP's DIDComm handler authenticates on the authcrypt + // sender and reads nothing here. See the note in didcomm.ts about the + // conformance gap this leaves against the canonical schema. + assert.deepEqual(opened.message.body, {}); +});