Files
matrix-quotes-bot/node_modules/matrix-js-sdk/lib/oauth/index.js

253 lines
11 KiB
JavaScript
Raw Normal View History

2026-09-12 23:57:45 -04:00
import _objectSpread from "@babel/runtime/helpers/objectSpread2";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
/*
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 { generateScope, normalizeBearerTokenResponseTokenType, startDeviceAuthorization, validateBearerTokenResponse, waitForDeviceAuthorization } from "./authorize.js";
import { OAuthGrantType, urlHasCommonBase, validateRegistrationResponse } from "./register.js";
import { encodeUnpaddedBase64Url } from "../base64.js";
import { sha256 } from "../digest.js";
import { HTTPError, isMatrixErrorResponse, MatrixError, Method } from "../http-api/index.js";
import { logger } from "../logger.js";
import { isOAuth2ErrorResponse, OAuth2Error, OAuth2HTTPError } from "./error.js";
import { secureRandomString } from "../randomstring.js";
export * from "./authorize.js";
export * from "./error.js";
export * from "./register.js";
export * from "./tokenRefresher.js";
export * from "./discover.js";
/**
* Type representing the persistent context needed for typical OAuth flows
*/
export class OAuth2 {
/**
* Attempts dynamic registration against the configured registration endpoint.
* Will ignore any URIs that do not use client_uri as a common base as per the spec.
* @param authMetadata - Auth config from {@link MatrixClient.getAuthMetadata}
* @param clientMetadata - The metadata for the client which to register,
* grant_types & response_types & token_endpoint_auth_method will be sanely calculated if omitted.
* @returns Promise<string> resolved with registered clientId
* @throws when registration is not supported, on failed request or invalid response
*/
static async registerClient(authMetadata, clientMetadata) {
const defaultGrantTypes = [OAuthGrantType.AuthorizationCode, OAuthGrantType.RefreshToken];
// ask for device authorization grant if supported
if (authMetadata.grant_types_supported.includes(OAuthGrantType.DeviceAuthorization)) {
defaultGrantTypes.push(OAuthGrantType.DeviceAuthorization);
}
const grantTypes = clientMetadata.grant_types ?? defaultGrantTypes;
if (grantTypes.some(scope => !authMetadata.grant_types_supported.includes(scope))) {
throw new Error(OAuth2Error.DynamicRegistrationNotSupported);
}
const commonBase = new URL(clientMetadata.client_uri);
const request = _objectSpread(_objectSpread({
// Apply some defaults
response_types: ["code"],
token_endpoint_auth_method: "none"
}, clientMetadata), {}, {
grant_types: grantTypes,
logo_uri: urlHasCommonBase(commonBase, clientMetadata.logo_uri) ? clientMetadata.logo_uri : undefined,
policy_uri: urlHasCommonBase(commonBase, clientMetadata.policy_uri) ? clientMetadata.policy_uri : undefined,
tos_uri: urlHasCommonBase(commonBase, clientMetadata.tos_uri) ? clientMetadata.tos_uri : undefined
});
try {
const response = await fetch(authMetadata.registration_endpoint, {
method: Method.Post,
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(request)
});
if (response.status >= 400) {
throw new Error(OAuth2Error.DynamicRegistrationFailed);
}
const registrationResponse = await response.json();
if (validateRegistrationResponse(registrationResponse)) {
return registrationResponse.client_id;
}
throw new Error(OAuth2Error.DynamicRegistrationInvalid);
} catch (error) {
if (Object.values(OAuth2Error).includes(error.message)) {
throw error;
} else {
logger.error("Dynamic registration request failed", error);
throw new Error(OAuth2Error.DynamicRegistrationFailed, {
cause: error
});
}
}
}
constructor(metadata, context) {
_defineProperty(this, "context", void 0);
this.metadata = metadata;
this.context = {
clientId: context.clientId,
redirectUri: context.redirectUri,
deviceId: context.deviceId ?? secureRandomString(10),
codeVerifier: context.codeVerifier ?? secureRandomString(96)
};
}
/**
* Generate a URL to attempt authorization with the OP
* See https://spec.matrix.org/v1.18/client-server-api/#authorization-code-flow
* @param state - A unique opaque identifier, like a transaction ID,
* that will allow the client to maintain state between the authorization request and the callback.
* The app should use this to key the storage for where the rest of the auth context is saved.
* @param responseMode - The manner in which the IdP should send the secrets back to the app. Defaults to `fragment` for privacy.
* @param prompt - Optional prompt parameter to pass to the IdP to signal intent, e.g. `create` for User registration.
* @param scope - The OAuth2 scope to request, will be generated based on the device ID if omitted.
* @returns a Promise with the url as a string
*/
async generateAuthorizationCodeGrantUrl(state, responseMode = "fragment", prompt, scope) {
const challenge = encodeUnpaddedBase64Url(await sha256(this.context.codeVerifier));
const url = new URL(this.metadata.authorization_endpoint);
url.searchParams.set("response_type", "code");
url.searchParams.set("response_mode", responseMode);
url.searchParams.set("client_id", this.context.clientId);
url.searchParams.set("redirect_uri", this.context.redirectUri);
url.searchParams.set("scope", scope ?? generateScope(this.context.deviceId));
url.searchParams.set("state", state);
url.searchParams.set("code_challenge_method", "S256");
url.searchParams.set("code_challenge", challenge);
if (prompt) {
url.searchParams.set("prompt", prompt);
}
return url.toString();
}
/**
* Attempt to exchange authorization code for bearer token.
*
* Takes the authorization code returned by the OAuth2 Provider via the authorization URL, and makes a
* request to the Token Endpoint, to obtain the access token, refresh token, etc.
*
* @param code - authorization code as returned by IdP during authorization
* @returns a validated bearer token response
* @throws An `Error` with `message` set to an entry in {@link OAuth2Error},
* when the request fails, or the returned token response is invalid.
*/
async completeAuthorizationCodeGrant(code) {
const params = new URLSearchParams();
params.append("grant_type", "authorization_code");
params.append("client_id", this.context.clientId);
params.append("code_verifier", this.context.codeVerifier);
params.append("redirect_uri", this.context.redirectUri);
params.append("code", code);
const tokenResponse = await this.fetch("token", params, OAuth2Error.CodeExchangeFailed);
// throws when response is invalid
validateBearerTokenResponse(tokenResponse);
return normalizeBearerTokenResponseTokenType(tokenResponse);
}
/**
* Refresh the access token using the given refresh token and the refresh token grant
* @param refreshToken - the token to use to refresh the access token
*/
async performRefreshTokenGrant(refreshToken) {
const params = new URLSearchParams();
params.append("grant_type", "refresh_token");
params.append("client_id", this.context.clientId);
params.append("refresh_token", refreshToken);
const tokenResponse = await this.fetch("token", params, OAuth2Error.RefreshTokenFailed);
// throws when response is invalid
validateBearerTokenResponse(tokenResponse);
return normalizeBearerTokenResponseTokenType(tokenResponse);
}
/**
* Revokes the given token
* @param token - the token to remove
* @param type - the type of token, acts as a hint to the IdP
*/
async revokeToken(token, type) {
const params = new URLSearchParams();
params.append("token", token);
params.append("client_id", this.context.clientId);
if (type) {
params.append("token_type_hint", type);
}
await this.fetch("revocation", params, OAuth2Error.RevokeTokenFailed);
const headers = new Headers();
headers.set("Content-Type", "application/x-www-form-urlencoded");
}
/**
* Begin OAuth2 device authorization flow.
* @param scope - the scope to request for authorization.
* @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.
*/
async startDeviceAuthorizationGrant(scope) {
return startDeviceAuthorization({
scope: scope ?? generateScope(this.context.deviceId),
metadata: this.metadata,
clientId: this.context.clientId
});
}
/**
* Polls the OAuth2 token endpoint until we get a device access token response, or encounter an unrecoverable error.
* @param session - The session returned from a previous call to {@link OAuth2.startDeviceAuthorizationGrant}.
* @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.
*/
async waitForDeviceAuthorizationGrant(session) {
return waitForDeviceAuthorization({
session,
metadata: this.metadata,
clientId: this.context.clientId
});
}
async fetch(target, params, error) {
const url = this.metadata[`${target}_endpoint`];
const res = await fetch(url, {
method: Method.Post,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json"
},
body: params
});
if (res.status >= 400) {
let body;
try {
body = await res.json();
} catch {
// The endpoint didn't give us a JSON body, so we can't determine the error type. We'll throw a generic
// HTTPError below.
}
// Because the Matrix C-S API error response format is so similar to the OAuth 2.0 error response format
// the ordering of these checks is important. We want to check for a Matrix error response first, and only
// if it isn't one do we check for an OAuth 2.0 error response.
// This essentially relies on `errcode` not being present in an OAuth 2.0 error response.
if (isMatrixErrorResponse(body)) {
throw new MatrixError(body, res.status, undefined, undefined, res.headers);
}
if (isOAuth2ErrorResponse(body)) {
throw new OAuth2HTTPError(error, res.status, res.headers, body);
}
throw new HTTPError(error, res.status, res.headers);
}
return await res.json();
}
}
//# sourceMappingURL=index.js.map