feat(auth): include authentication in workspace and tht diagnostics
This commit is contained in:
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user