diff --git a/src/index.ts b/src/index.ts index d31e136..c728b69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ export * as agent from "./agent.ts"; export * as verifier from "./verifier.ts"; export { X401ValidationError } from "./validate.ts"; -export type { ReturnUriOptions } from "./validate.ts"; +export type { InsecureUriOptions, ReturnUriOptions } from "./validate.ts"; export { ACCESS_TOKEN_TYPE, diff --git a/src/validate.ts b/src/validate.ts index a2516be..4ddcc6e 100644 --- a/src/validate.ts +++ b/src/validate.ts @@ -24,14 +24,27 @@ function isString(value: unknown): value is string { const DC_API_PROTOCOLS: readonly string[] = Object.values(DC_API_PROTOCOL); -export interface ReturnUriOptions { +export interface InsecureUriOptions { /** - * Skip the https requirement on `return_uri`, permitting an http URL (e.g. a - * `http://localhost` dev transport). Defaults to false. + * Skip the https requirement on `return_uri` / `credential_result_uri`, + * permitting an http URL (e.g. a `http://localhost` dev transport). Defaults + * to false. */ allowInsecureUri?: boolean; } +export type ReturnUriOptions = InsecureUriOptions; + +function isAllowedUri( + value: unknown, + options?: InsecureUriOptions, +): value is string { + return ( + isString(value) && + (options?.allowInsecureUri === true || value.startsWith("https://")) + ); +} + /** * Asserts `return_uri` is a string and, unless `allowInsecureUri` is set, an * https URL. Shared by the intermediary that adds it and the recipient that @@ -41,10 +54,7 @@ export function assertReturnUri( returnUri: unknown, options?: ReturnUriOptions, ): void { - if ( - !isString(returnUri) || - (!options?.allowInsecureUri && !returnUri.startsWith("https://")) - ) { + if (!isAllowedUri(returnUri, options)) { throw new X401ValidationError("return_uri must be an https URL."); } } @@ -113,7 +123,10 @@ function assertOAuthMetadata(oauth: unknown): void { } } -export function parseResultArtifact(value: unknown): ResultArtifact { +export function parseResultArtifact( + value: unknown, + options?: InsecureUriOptions, +): ResultArtifact { if (!isObject(value)) { throw new X401ValidationError("Result Artifact must be a JSON object."); } @@ -136,11 +149,7 @@ export function parseResultArtifact(value: unknown): ResultArtifact { ); } } - if ( - hasUri && - (!isString(value.credential_result_uri) || - !value.credential_result_uri.startsWith("https://")) - ) { + if (hasUri && !isAllowedUri(value.credential_result_uri, options)) { throw new X401ValidationError( "Result Artifact credential_result_uri must be an https URL.", ); diff --git a/src/verifier.ts b/src/verifier.ts index 136b58b..5b5fdc0 100644 --- a/src/verifier.ts +++ b/src/verifier.ts @@ -13,6 +13,7 @@ import { parseX401TokenObject, X401ValidationError, } from "./validate.ts"; +import type { InsecureUriOptions } from "./validate.ts"; import type { CredentialRequestOptions, OAuthMetadata, @@ -88,8 +89,11 @@ function escapeHtml(value: string): string { return value.replace(/&/g, "&").replace(/ { ); }); +test("Result Artifact allows an http result uri when allowInsecureUri is set", () => { + const insecure = Buffer.from( + JSON.stringify({ + credential_result_uri: "http://localhost:3080/x401/result?ref=abc", + }), + ).toString("base64url"); + const decoded = verifier.decodeResultArtifact(insecure, { + allowInsecureUri: true, + }); + assert.equal( + decoded.credential_result_uri, + "http://localhost:3080/x401/result?ref=abc", + ); +}); + +test("Result Artifact still rejects a non-string result uri under allowInsecureUri", () => { + const bad = Buffer.from( + JSON.stringify({ credential_result_uri: 42 }), + ).toString("base64url"); + assert.throws( + () => verifier.decodeResultArtifact(bad, { allowInsecureUri: true }), + X401ValidationError, + ); +}); + test("Result Artifact with both result and uri is rejected", () => { const both = Buffer.from( JSON.stringify({