Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
35 changes: 22 additions & 13 deletions src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.");
}
}
Expand Down Expand Up @@ -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.");
}
Expand All @@ -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.",
);
Expand Down
8 changes: 6 additions & 2 deletions src/verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
parseX401TokenObject,
X401ValidationError,
} from "./validate.ts";
import type { InsecureUriOptions } from "./validate.ts";
import type {
CredentialRequestOptions,
OAuthMetadata,
Expand Down Expand Up @@ -88,8 +89,11 @@ function escapeHtml(value: string): string {
return value.replace(/&/g, "&amp;").replace(/</g, "&lt;");
}

export function decodeResultArtifact(headerValue: string): ResultArtifact {
return parseResultArtifact(decodeProofHeader(headerValue));
export function decodeResultArtifact(
headerValue: string,
options?: InsecureUriOptions,
): ResultArtifact {
return parseResultArtifact(decodeProofHeader(headerValue), options);
}

export function decodeTokenObject(headerValue: string): X401TokenObject {
Expand Down
25 changes: 25 additions & 0 deletions tests/x401.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,31 @@ test("Result Artifact with a non-https result uri is rejected", () => {
);
});

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({
Expand Down
Loading