init
This commit is contained in:
439
node_modules/matrix-js-sdk/lib/rendezvous/MSC4108SignInWithQR.js
generated
vendored
Normal file
439
node_modules/matrix-js-sdk/lib/rendezvous/MSC4108SignInWithQR.js
generated
vendored
Normal file
@@ -0,0 +1,439 @@
|
||||
import _objectSpread from "@babel/runtime/helpers/objectSpread2";
|
||||
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
||||
/*
|
||||
Copyright 2024 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 { QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import { ClientRendezvousFailureReason, MSC4108FailureReason, RendezvousError } from "./index.js";
|
||||
import { logger } from "../logger.js";
|
||||
import { MatrixError } from "../http-api/index.js";
|
||||
import { sleep } from "../utils.js";
|
||||
import { generateScope, OAuthGrantType, startDeviceAuthorization, waitForDeviceAuthorization } from "../oauth/index.js";
|
||||
/**
|
||||
* Enum representing the payload types transmissible over [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108)
|
||||
* secure channels.
|
||||
* @experimental Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
*/
|
||||
export let PayloadType = /*#__PURE__*/function (PayloadType) {
|
||||
PayloadType["Protocols"] = "m.login.protocols";
|
||||
PayloadType["Protocol"] = "m.login.protocol";
|
||||
PayloadType["Failure"] = "m.login.failure";
|
||||
PayloadType["Success"] = "m.login.success";
|
||||
PayloadType["Secrets"] = "m.login.secrets";
|
||||
PayloadType["ProtocolAccepted"] = "m.login.protocol_accepted";
|
||||
PayloadType["Declined"] = "m.login.declined";
|
||||
return PayloadType;
|
||||
}({});
|
||||
|
||||
/**
|
||||
* Type representing the base payload format for [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108)
|
||||
* messages sent over the secure channel.
|
||||
* @experimental Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
*/
|
||||
|
||||
function isDeviceAuthorizationGrantProtocolPayload(payload) {
|
||||
return payload.protocol === "device_authorization_grant";
|
||||
}
|
||||
/**
|
||||
* Prototype of the unstable [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108)
|
||||
* sign in with QR + OAuth2 flow.
|
||||
* @experimental Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
*/
|
||||
export class MSC4108SignInWithQR {
|
||||
/**
|
||||
* Returns the check code for the secure channel or undefined if not generated yet.
|
||||
*/
|
||||
get checkCode() {
|
||||
return this.channel?.getCheckCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param channel - The secure channel used for communication
|
||||
* @param client - The Matrix client in used on the device already logged in
|
||||
* @param didScanCode - Whether this side of the channel scanned the QR code from the other party
|
||||
* @param onFailure - Callback for when the rendezvous fails
|
||||
*/
|
||||
constructor(channel, didScanCode, client, onFailure) {
|
||||
_defineProperty(this, "ourIntent", void 0);
|
||||
_defineProperty(this, "_code", void 0);
|
||||
_defineProperty(this, "expectingNewDeviceId", void 0);
|
||||
_defineProperty(this, "metadata", void 0);
|
||||
_defineProperty(this, "grantInProgress", void 0);
|
||||
this.channel = channel;
|
||||
this.didScanCode = didScanCode;
|
||||
this.client = client;
|
||||
this.onFailure = onFailure;
|
||||
this.ourIntent = client ? QrCodeIntent.Reciprocate : QrCodeIntent.Login;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet.
|
||||
*/
|
||||
get code() {
|
||||
return this._code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the code including doing partial set up of the channel where required.
|
||||
*/
|
||||
async generateCode() {
|
||||
if (this._code) {
|
||||
return;
|
||||
}
|
||||
if (this.ourIntent === QrCodeIntent.Reciprocate && this.client) {
|
||||
this._code = await this.channel.generateCode(this.ourIntent, this.client.getDomain());
|
||||
} else if (this.ourIntent === QrCodeIntent.Login) {
|
||||
this._code = await this.channel.generateCode(this.ourIntent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the device is the already logged in device reciprocating a new login on the other side of the channel.
|
||||
*/
|
||||
get isExistingDevice() {
|
||||
return this.ourIntent === QrCodeIntent.Reciprocate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the device is the new device logging in being reciprocated by the device on the other side of the channel.
|
||||
*/
|
||||
get isNewDevice() {
|
||||
return !this.isExistingDevice;
|
||||
}
|
||||
|
||||
/**
|
||||
* The first step in the OAuth2 QR login process.
|
||||
* To be called after the QR code has been rendered or scanned.
|
||||
* The scanning device has to discover the homeserver details, if they scanned the code then they already have it.
|
||||
* If the new device is the one rendering the QR code then it has to wait be sent the homeserver details via the rendezvous channel.
|
||||
*/
|
||||
async negotiateProtocols() {
|
||||
logger.info(`negotiateProtocols(isNewDevice=${this.isNewDevice} didScanCode=${this.didScanCode})`);
|
||||
await this.channel.connect();
|
||||
if (this.didScanCode) {
|
||||
// Secure Channel step 6 completed, we trust the channel
|
||||
|
||||
if (this.isNewDevice) {
|
||||
// MSC4108-Flow: ExistingScanned - take homeserver from QR code which should already be set
|
||||
} else {
|
||||
// MSC4108-Flow: NewScanned -send protocols message
|
||||
let authMetadata;
|
||||
try {
|
||||
authMetadata = await this.client.getAuthMetadata();
|
||||
} catch (e) {
|
||||
logger.error("Failed to discover OAuth2 metadata", e);
|
||||
}
|
||||
if (authMetadata?.grant_types_supported.includes(OAuthGrantType.DeviceAuthorization)) {
|
||||
await this.send({
|
||||
type: PayloadType.Protocols,
|
||||
protocols: ["device_authorization_grant"],
|
||||
homeserver: this.client.getDomain()
|
||||
});
|
||||
} else {
|
||||
await this.send({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.UnsupportedProtocol
|
||||
});
|
||||
throw new RendezvousError("Device code grant unsupported", MSC4108FailureReason.UnsupportedProtocol);
|
||||
}
|
||||
}
|
||||
} else if (this.isNewDevice) {
|
||||
// MSC4108-Flow: ExistingScanned - wait for protocols message
|
||||
logger.info("Waiting for protocols message");
|
||||
const payload = await this.receive();
|
||||
if (payload?.type === PayloadType.Failure) {
|
||||
throw new RendezvousError("Failed", payload.reason);
|
||||
}
|
||||
if (payload?.type !== PayloadType.Protocols) {
|
||||
await this.send({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.UnexpectedMessageReceived
|
||||
});
|
||||
throw new RendezvousError("Unexpected message received", MSC4108FailureReason.UnexpectedMessageReceived);
|
||||
}
|
||||
return {
|
||||
serverName: payload.homeserver
|
||||
};
|
||||
} else {
|
||||
// MSC4108-Flow: NewScanned - nothing to do
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* The second & third step in the OAuth2 QR login process.
|
||||
* To be called after `negotiateProtocols` for the existing device.
|
||||
* To be called after OAuth2 negotiation for the new device.
|
||||
*
|
||||
* @param input - Required for the new device to start the device authorization grant, not required for the existing device reciprocating the login
|
||||
*/
|
||||
async deviceAuthorizationGrant(input) {
|
||||
if (this.isNewDevice) {
|
||||
if (!input) {
|
||||
throw new Error("Input must be provided for new device");
|
||||
}
|
||||
const {
|
||||
metadata,
|
||||
clientId,
|
||||
deviceId
|
||||
} = input;
|
||||
const scope = generateScope(deviceId);
|
||||
|
||||
// MSC4108-Flow: NewDevice - start device authorization grant
|
||||
const dagResponse = await startDeviceAuthorization({
|
||||
clientId,
|
||||
scope,
|
||||
metadata
|
||||
});
|
||||
this.metadata = metadata;
|
||||
this.grantInProgress = dagResponse;
|
||||
const protocol = {
|
||||
type: PayloadType.Protocol,
|
||||
protocol: "device_authorization_grant",
|
||||
device_id: deviceId,
|
||||
device_authorization_grant: {
|
||||
verification_uri: dagResponse.verification_uri,
|
||||
verification_uri_complete: dagResponse.verification_uri_complete
|
||||
}
|
||||
};
|
||||
await this.send(protocol);
|
||||
return {
|
||||
verificationUri: dagResponse.verification_uri_complete ?? dagResponse.verification_uri,
|
||||
userCode: dagResponse.user_code
|
||||
};
|
||||
} else {
|
||||
// The user needs to do step 7 for the out-of-band confirmation
|
||||
// but, first we receive the protocol chosen by the other device so that
|
||||
// the confirmation_uri is ready to go
|
||||
logger.info("Waiting for protocol message");
|
||||
const payload = await this.receive();
|
||||
if (payload?.type === PayloadType.Failure) {
|
||||
throw new RendezvousError("Failed", payload.reason);
|
||||
}
|
||||
if (payload?.type !== PayloadType.Protocol) {
|
||||
await this.send({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.UnexpectedMessageReceived
|
||||
});
|
||||
throw new RendezvousError("Unexpected message received", MSC4108FailureReason.UnexpectedMessageReceived);
|
||||
}
|
||||
if (isDeviceAuthorizationGrantProtocolPayload(payload)) {
|
||||
const {
|
||||
device_authorization_grant: dag,
|
||||
device_id: expectingNewDeviceId
|
||||
} = payload;
|
||||
const {
|
||||
verification_uri: verificationUri,
|
||||
verification_uri_complete: verificationUriComplete
|
||||
} = dag;
|
||||
let deviceAlreadyExists = true;
|
||||
try {
|
||||
await this.client?.getDevice(expectingNewDeviceId);
|
||||
} catch (err) {
|
||||
if (err instanceof MatrixError && err.httpStatus === 404) {
|
||||
deviceAlreadyExists = false;
|
||||
}
|
||||
}
|
||||
if (deviceAlreadyExists) {
|
||||
await this.send({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.DeviceAlreadyExists
|
||||
});
|
||||
throw new RendezvousError("Specified device ID already exists", MSC4108FailureReason.DeviceAlreadyExists);
|
||||
}
|
||||
this.expectingNewDeviceId = expectingNewDeviceId;
|
||||
return {
|
||||
verificationUri: verificationUriComplete ?? verificationUri
|
||||
};
|
||||
}
|
||||
await this.send({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.UnsupportedProtocol
|
||||
});
|
||||
throw new RendezvousError("Received a request for an unsupported protocol", MSC4108FailureReason.UnsupportedProtocol);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The fourth step in the OAuth2 QR login process.
|
||||
* The reciprocating device must perform step 5 for this method to resolve.
|
||||
* To be called after {@link deviceAuthorizationGrant} only on the new device.
|
||||
*/
|
||||
async completeLoginOnNewDevice({
|
||||
clientId
|
||||
}) {
|
||||
if (!this.isNewDevice || !this.grantInProgress || !this.metadata) {
|
||||
throw new Error("Can only complete login on new device");
|
||||
}
|
||||
logger.info("Waiting for protocol accepted message");
|
||||
// wait for accepted message
|
||||
const payload = await this.receive();
|
||||
if (!payload) {
|
||||
throw new RendezvousError("No response from existing device", MSC4108FailureReason.UnexpectedMessageReceived);
|
||||
}
|
||||
if (payload.type === PayloadType.Failure) {
|
||||
throw new RendezvousError("Failed", payload.reason);
|
||||
}
|
||||
if (payload.type !== PayloadType.ProtocolAccepted) {
|
||||
throw new RendezvousError("Unexpected message received", MSC4108FailureReason.UnexpectedMessageReceived);
|
||||
}
|
||||
|
||||
// poll for DAG
|
||||
const res = await waitForDeviceAuthorization({
|
||||
session: this.grantInProgress,
|
||||
metadata: this.metadata,
|
||||
clientId
|
||||
});
|
||||
if (!res) {
|
||||
throw new RendezvousError("No response from device authorization endpoint", ClientRendezvousFailureReason.Unknown);
|
||||
}
|
||||
if ("error" in res) {
|
||||
let reason = MSC4108FailureReason.UnexpectedMessageReceived;
|
||||
if (res.error === "expired_token") {
|
||||
reason = MSC4108FailureReason.AuthorizationExpired;
|
||||
} else if (res.error === "access_denied") {
|
||||
reason = MSC4108FailureReason.UserCancelled;
|
||||
}
|
||||
await this.send({
|
||||
type: PayloadType.Failure,
|
||||
reason
|
||||
});
|
||||
throw new RendezvousError("Rejection from device authorization endpoint", reason);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* The fifth (and final) step in the OAuth2 QR login process.
|
||||
* To be called after the new device has completed authentication.
|
||||
*/
|
||||
async shareSecrets() {
|
||||
if (this.isNewDevice) {
|
||||
await this.send({
|
||||
type: PayloadType.Success
|
||||
});
|
||||
// then wait for secrets
|
||||
logger.info("Waiting for secrets message");
|
||||
const payload = await this.receive();
|
||||
if (payload?.type === PayloadType.Failure) {
|
||||
throw new RendezvousError("Failed", payload.reason);
|
||||
}
|
||||
if (payload?.type !== PayloadType.Secrets) {
|
||||
await this.send({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.UnexpectedMessageReceived
|
||||
});
|
||||
throw new RendezvousError("Unexpected message received", MSC4108FailureReason.UnexpectedMessageReceived);
|
||||
}
|
||||
return {
|
||||
secrets: payload
|
||||
};
|
||||
// then done?
|
||||
} else {
|
||||
if (!this.expectingNewDeviceId) {
|
||||
throw new Error("No new device ID expected");
|
||||
}
|
||||
await this.send({
|
||||
type: PayloadType.ProtocolAccepted
|
||||
});
|
||||
logger.info("Waiting for outcome message");
|
||||
const payload = await this.receive();
|
||||
if (payload?.type === PayloadType.Failure) {
|
||||
throw new RendezvousError("Failed", payload.reason);
|
||||
}
|
||||
if (payload?.type === PayloadType.Declined) {
|
||||
throw new RendezvousError("User declined", ClientRendezvousFailureReason.UserDeclined);
|
||||
}
|
||||
if (payload?.type !== PayloadType.Success) {
|
||||
await this.send({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.UnexpectedMessageReceived
|
||||
});
|
||||
throw new RendezvousError("Unexpected message", MSC4108FailureReason.UnexpectedMessageReceived);
|
||||
}
|
||||
const timeout = Date.now() + 10000; // wait up to 10 seconds
|
||||
do {
|
||||
// is the device visible via the Homeserver?
|
||||
try {
|
||||
const device = await this.client?.getDevice(this.expectingNewDeviceId);
|
||||
if (device) {
|
||||
// if so, return the secrets
|
||||
const secretsBundle = await this.client.getCrypto().exportSecretsBundle();
|
||||
if (this.channel.cancelled) {
|
||||
throw new RendezvousError("User cancelled", MSC4108FailureReason.UserCancelled);
|
||||
}
|
||||
// send secrets
|
||||
await this.send(_objectSpread({
|
||||
type: PayloadType.Secrets
|
||||
}, secretsBundle));
|
||||
return {
|
||||
secrets: secretsBundle
|
||||
};
|
||||
// let the other side close the rendezvous session
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof MatrixError && err.httpStatus === 404) {
|
||||
// not found, so keep waiting until timeout
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
await sleep(1000);
|
||||
} while (Date.now() < timeout);
|
||||
await this.send({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.DeviceNotFound
|
||||
});
|
||||
throw new RendezvousError("New device not found", MSC4108FailureReason.DeviceNotFound);
|
||||
}
|
||||
}
|
||||
async receive() {
|
||||
return await this.channel.secureReceive();
|
||||
}
|
||||
async send(payload) {
|
||||
await this.channel.secureSend(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decline the login on the existing device.
|
||||
*/
|
||||
async declineLoginOnExistingDevice() {
|
||||
if (!this.isExistingDevice) {
|
||||
throw new Error("Can only decline login on existing device");
|
||||
}
|
||||
await this.send({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.UserCancelled
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the rendezvous session.
|
||||
* @param reason the reason for the cancellation
|
||||
*/
|
||||
async cancel(reason) {
|
||||
this.onFailure?.(reason);
|
||||
await this.channel.cancel(reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the rendezvous session.
|
||||
*/
|
||||
async close() {
|
||||
await this.channel.close();
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=MSC4108SignInWithQR.js.map
|
||||
Reference in New Issue
Block a user