Files
matrix-quotes-bot/node_modules/matrix-js-sdk/lib/oauth/authorize.js
unfunny dfe4cc537f init
2026-09-13 00:29:11 -04:00

193 lines
7.5 KiB
JavaScript

import _objectSpread from "@babel/runtime/helpers/objectSpread2";
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { secureRandomString } from "../randomstring.js";
import { OAuth2Error } from "./error.js";
import { hasOptionalNumberProperty, hasOptionalStringProperty, hasRequiredNumberProperty, hasRequiredStringProperty, isRecord } from "../@types/type-guards.js";
import { Method } from "../http-api/index.js";
import { OAuthGrantType } from "./register.js";
import { sleep } from "../utils.js";
/**
* The expected response type from the token endpoint during authorization code flow
* Normalized to always use capitalized 'Bearer' for token_type
*
* See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4
*/
/**
* Metadata from OAuth 2.0 token_endpoint as per
* https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
* With validated properties required in type
*
* This response is expected for the authorization code grant and refresh token grant,
* as defined in the Matrix spec.
*/
/**
* Validate the given response matches the format expected for a {@link ValidTokenResponse}
* @param response - the response to validate
* @throws if the response does not match the expected format
*/
export function validateBearerTokenResponse(response) {
if (!isRecord(response) || !hasRequiredStringProperty(response, "token_type") ||
// token_type is case-insensitive, some OPs return `token_type: "bearer"`
response["token_type"].toLowerCase() !== "bearer" || !hasRequiredStringProperty(response, "access_token") || !hasOptionalNumberProperty(response, "expires_in") || !hasOptionalStringProperty(response, "refresh_token") || !hasOptionalStringProperty(response, "scope")) {
throw new Error(OAuth2Error.InvalidBearerTokenResponse);
}
}
/**
* Generate the scope used in authorization request with OAuth2 IdP
* @returns scope
*/
export const generateScope = deviceId => {
const safeDeviceId = deviceId ?? secureRandomString(10);
return `urn:matrix:client:api:* urn:matrix:client:device:${safeDeviceId}`;
};
/**
* Normalize token_type to use capital case to make consuming the token response easier
* token_type is case insensitive, and it is spec-compliant for OPs to return token_type: "bearer"
* Later, when used in auth headers it is case sensitive and must be Bearer
* See: https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4
*
* @param response - validated token response
* @returns response with token_type set to 'Bearer'
*/
export const normalizeBearerTokenResponseTokenType = response => _objectSpread(_objectSpread({}, response), {}, {
token_type: "Bearer"
});
/**
* Response from the OAuth2 token endpoint when exchanging a token for grant_type device_code.
*/
/**
* Validate the given response matches the format expected for a {@link DeviceAccessTokenResponse}
* @param response - the response to validate
* @throws if the response does not match the expected format
*/
export function isValidDeviceAccessTokenResponse(response) {
return isRecord(response) && hasRequiredStringProperty(response, "access_token") && hasRequiredStringProperty(response, "token_type") && hasOptionalStringProperty(response, "refresh_token") && hasOptionalStringProperty(response, "scope") && hasOptionalNumberProperty(response, "expires_in");
}
/**
* Error from the OAuth2 token endpoint when exchanging a token for grant_type device_code.
*/
/**
* Response from the OAuth2 device authorization endpoint.
* As specified in https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
*/
/**
* Validate the given response matches the format expected for a {@link DeviceAuthorizationResponse}
* @param response - the response to validate
* @throws if the response does not match the expected format
*/
export function validateDeviceAuthorizationResponse(response) {
if (!isRecord(response) || !hasRequiredStringProperty(response, "device_code") || !hasRequiredStringProperty(response, "user_code") || !hasRequiredStringProperty(response, "verification_uri") || !hasRequiredNumberProperty(response, "expires_in") || !hasOptionalStringProperty(response, "verification_uri_complete") || !hasOptionalNumberProperty(response, "interval")) {
throw new Error(OAuth2Error.InvalidDeviceAuthorizationResponse);
}
}
/**
* Begin OAuth2 device authorization flow.
* @param options - The device authorization parameters.
* @param options.clientId - the client ID returned from client registration.
* @param options.scope - the scope to request for authorization.
* @param options.metadata - the validated OAuth2 metadata for the Identity Provider.
* @returns a promise that resolves to a device access token response,
* or an error response if the user denies authorization or the device code expires.
*/
export const startDeviceAuthorization = async ({
clientId,
scope,
metadata
}) => {
const body = new URLSearchParams({
client_id: clientId,
scope: scope
}).toString();
const url = metadata.device_authorization_endpoint;
if (!url) {
throw new Error("No device_authorization_endpoint given");
}
const response = await fetch(url, {
method: Method.Post,
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body
});
const data = await response.json();
validateDeviceAuthorizationResponse(data);
return data;
};
/**
* Polls the OAuth2 token endpoint until we get a device access token response, or encounter an unrecoverable error.
* @param options - The device authorization parameters.
* @param options.session - The session returned from a previous call to {@link startDeviceAuthorization}.
* @param options.metadata - The validated OAuth2 metadata for the Identity Provider.
* @param options.clientId - The client ID returned from client registration.
* @returns a promise that resolves to a device access token response,
* or an error response if the user denies authorization or the device code expires.
*/
export const waitForDeviceAuthorization = async ({
session,
metadata,
clientId
}) => {
let interval = (session.interval ?? 5) * 1000; // poll interval
const expiration = Date.now() + session.expires_in * 1000;
do {
const body = new URLSearchParams({
device_code: session.device_code,
grant_type: OAuthGrantType.DeviceAuthorization,
client_id: clientId
}).toString();
const response = await fetch(metadata.token_endpoint, {
method: Method.Post,
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body
});
const data = await response.json();
if (response.ok && isValidDeviceAccessTokenResponse(data)) {
return data;
}
const errorResponse = data;
switch (errorResponse.error) {
case "authorization_pending":
break;
case "slow_down":
interval += 5000;
break;
case "access_denied":
case "expired_token":
return errorResponse;
}
await sleep(interval);
} while (Date.now() < expiration);
return {
error: "expired"
};
};
//# sourceMappingURL=authorize.js.map