feat(auth): include authentication in workspace and tht diagnostics

This commit is contained in:
2026-08-17 15:51:16 +02:00
parent cb0e873ed7
commit 3ed00ff086
19 changed files with 1050 additions and 34 deletions
+41
View File
@@ -4,6 +4,8 @@ import {
calculatePKCECodeChallenge,
customFetch,
discovery,
initiateDeviceAuthorization,
pollDeviceAuthorizationGrant,
type Configuration,
type CustomFetch,
} from "openid-client";
@@ -57,6 +59,14 @@ export class OidcIssuerMismatchError extends OidcProtocolError {
}
}
/** Device authorization is optional OIDC metadata and must never fall back to a browser flow. */
export class OidcDeviceFlowUnavailableError extends OidcProtocolError {
constructor() {
super("oidc_device_flow_unavailable");
this.name = "OidcDeviceFlowUnavailableError";
}
}
export interface OidcProtocolOptions {
issuer: string;
clientId: string;
@@ -77,6 +87,7 @@ const DEFAULT_HTTP_TIMEOUT_MS = 5_000;
const MAX_HTTP_TIMEOUT_MS = 30_000;
const DEFAULT_JWKS_TIMEOUT_MS = 5_000;
const MAX_JWKS_TIMEOUT_MS = 30_000;
const MAX_DEVICE_FLOW_TIMEOUT_MS = 10 * 60_000;
const text = (value: unknown, maximum = 2048): value is string =>
typeof value === "string" && value.length > 0 && value.length <= maximum && !/\p{Cc}/u.test(value);
@@ -599,5 +610,35 @@ export function createOidcProtocol(options: OidcProtocolOptions): OidcProtocol {
}
signal.throwIfAborted();
},
async verifyDeviceFlow(signal, present) {
let config: Configuration;
try {
config = await configuration();
const endpoint = config.serverMetadata().device_authorization_endpoint;
httpsEndpoint(endpoint);
} catch (error) {
if (error instanceof OidcIssuerMismatchError || error instanceof OidcProviderUnavailableError) throw error;
throw new OidcDeviceFlowUnavailableError();
}
const deadline = AbortSignal.timeout(MAX_DEVICE_FLOW_TIMEOUT_MS);
const deviceSignal = AbortSignal.any([signal, deadline]);
try {
deviceSignal.throwIfAborted();
const device = await initiateDeviceAuthorization(config, { scope: options.scopes.join(" ") });
if (!text(device.verification_uri, 2048) || !text(device.user_code, 256)) {
throw new OidcDeviceFlowUnavailableError();
}
const verificationUri = httpsEndpoint(device.verification_uri);
present(verificationUri.href, device.user_code);
const tokens = await pollDeviceAuthorizationGrant(config, device, undefined, { signal: deviceSignal });
await verifyIdTokenSignature(tokens.id_token, config, transport, jwksTimeoutMs);
const claims = tokens.claims();
if (!claims || Array.isArray(claims)) throw new OidcProtocolError();
return identityFromClaims(claims as Record<string, unknown>, options);
} catch (error) {
if (error instanceof OidcDeviceFlowUnavailableError) throw error;
throw protocolFailure(error);
}
},
};
}