-
-
Notifications
You must be signed in to change notification settings - Fork 676
MSC4108 support OIDC QR code login #4134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 90 commits
Commits
Show all changes
101 commits
Select commit
Hold shift + click to select a range
151349f
Nonce is optional or not present in the id_token depending on the gra…
hughns 9f8b29d
Prototype for MSC4108
hughns 62e3982
misc
hughns 7c07234
Remove redundant change
t3chguy 582350f
Tweak rendezvous index
t3chguy 964020a
Switch to using rust-crypto backed SecureChannel implementation for M…
t3chguy f2ab31b
Remove debug logging
t3chguy a3be3a9
Iterate PR
t3chguy 8a57f92
Tweak getPostEndpoint
t3chguy 3ffd014
Switch to generating/parsing MSC4108 QR codes via Rust Crypto
t3chguy c59d4b4
Discard changes to src/crypto/verification/QRCode.ts
t3chguy 16db19f
Label flows and merge steps 2 and 3
hughns 784137b
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into hu…
t3chguy 168e005
Wire up rust-crypto qr secrets import/export
t3chguy 73ff790
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into hu…
t3chguy 956af05
Remove spurious console log
t3chguy 9add919
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into hu…
t3chguy c03d746
Free rust crypto structures
t3chguy 28980c0
Fix free throwing an error
t3chguy 9a00126
Check IdP supports device_code scope before requesting it
t3chguy 6b01af2
prettier
t3chguy cf999b7
Remove changes which rely on major oidc-client-ts upstream changes
t3chguy a490ebf
Fix copyrights
t3chguy 296ef6f
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3…
t3chguy 42953c8
Make tsc happier
t3chguy d1bc600
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3…
t3chguy a839a11
Rename m.login.accepted to m.login.protocol_accepted
hughns e414067
Improve test coverage
t3chguy 6ccf855
Remove sections related to scanning QR codes to simplify
t3chguy d7dad56
Add testing and streamline
t3chguy 3b924e4
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3…
t3chguy ba29d4e
Mock of checkCode
hughns 4b847b1
Implementation of option 3c for when to share secrets
hughns 9827061
Use more accurate return type for secureReceive()
hughns 2a47077
Split login step 4 and fix step 3 where didn't scan code
hughns e5d9437
Use check code from crypto-wasm
hughns c7af7ad
Support for MSC4108
hughns 8f8101b
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3…
t3chguy 6cdeb61
Discard changes to src/rendezvous/transports/MSC3886SimpleHttpRendezv…
t3chguy 4a783dc
Iterate UX
t3chguy 8bc7acd
Merge remote-tracking branch 'origin/t3chguy/oidc-qr-prototyping' int…
t3chguy 10ff23d
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3…
t3chguy f0c6269
Wait 10 seconds for new device to come online
hughns 806581b
Make type safe sends more elegant
hughns 2489229
Report errors back to other side and handle Failure
hughns 3b4f9a7
Additional error reasons
hughns 094cb46
Rename data_mismatch to insecure_channel_detected
hughns 6c5d4b6
Updated error codes to match MSC
hughns 099992b
Add description of ClientRendezvousFailureReasons
hughns 65d759f
delint
t3chguy 9c2e0f7
Fix types
t3chguy e504c03
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3…
t3chguy cc727c1
Consume feature branch of rust crypto wasm dep
t3chguy b2c9ce1
Add awful prepare-stacking workaround
t3chguy 4c13fcf
Fix test types
t3chguy 79e7087
Fix tests
t3chguy 7fb1e7e
Iterate
t3chguy 80bad22
Improve coverage
t3chguy 144f91a
Iterate
t3chguy 3f39ce2
Fix tests
t3chguy 620fc0f
delint
t3chguy 6647338
Improve coverage
t3chguy 141e421
Improve coverage
t3chguy 2982525
Improve coverage
t3chguy ab232c5
Improve coverage
t3chguy ef9064f
Improve coverage
t3chguy a398a94
Improve coverage
t3chguy b788fe8
Iterate
t3chguy 6bcb7c3
Handle etag missing state
t3chguy 6e617c3
Update tests to match MSC
t3chguy 1242108
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3…
t3chguy 0f53b3b
delint
t3chguy f616fce
Iterate
t3chguy 06cdc92
Fix tests
t3chguy 608f7dd
Simplify
t3chguy 402879a
Handle cancellation before sharing secrets
t3chguy 7c3663a
Remove redundant test
t3chguy 5dc93ba
Iterate
t3chguy 0792434
Iterate
t3chguy 1901169
Apply suggestions from code review
t3chguy 609a4d9
Merge branch 'develop' into t3chguy/oidc-qr-prototyping
t3chguy 24e09af
Merge branch 'develop' into t3chguy/oidc-qr-prototyping
t3chguy 2aac395
Bump @matrix-org/matrix-sdk-crypto-wasm to 90b63b84df65c19161f94049d8…
t3chguy 4da2e95
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3…
t3chguy c97ae08
prettier
t3chguy 030de05
Locally expire channel if we are in wait-send state
t3chguy bab027c
Merge branch 'develop' into t3chguy/oidc-qr-prototyping
t3chguy b2bb514
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3…
t3chguy 41bd197
Iterate
t3chguy b78a045
Merge branch 'develop' into t3chguy/oidc-qr-prototyping
t3chguy 583675a
DRY
t3chguy dbc9cd4
Add comment
t3chguy e6a6649
Merge remote-tracking branch 'origin/t3chguy/oidc-qr-prototyping' int…
t3chguy 6f253e6
Add comments
t3chguy e4a6209
Correctly handle m.login.declined after MSC clarification
t3chguy 1c9cce8
Iterate
t3chguy 71cfa80
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3…
t3chguy bdb24b4
Iterate
t3chguy 039f897
Iterate
t3chguy 8332cd0
Iterate
t3chguy 6f1ae84
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3…
t3chguy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,362 @@ | ||
| /* | ||
| 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 { QrCodeData, QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm"; | ||
| import { mocked } from "jest-mock"; | ||
| import fetchMock from "fetch-mock-jest"; | ||
|
|
||
| import { | ||
| MSC4108FailureReason, | ||
| MSC4108RendezvousSession, | ||
| MSC4108SecureChannel, | ||
| MSC4108SignInWithQR, | ||
| PayloadType, | ||
| } from "../../../src/rendezvous"; | ||
| import { defer } from "../../../src/utils"; | ||
| import { | ||
| ClientPrefix, | ||
| DEVICE_CODE_SCOPE, | ||
| IHttpOpts, | ||
| IMyDevice, | ||
| MatrixClient, | ||
| MatrixError, | ||
| MatrixHttpApi, | ||
| } from "../../../src"; | ||
| import { mockOpenIdConfiguration } from "../../test-utils/oidc"; | ||
|
|
||
| function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient { | ||
| const baseUrl = "https://example.com"; | ||
| const crypto = { | ||
| exportSecretsForQrLogin: jest.fn(), | ||
| }; | ||
| const client = { | ||
| doesServerSupportUnstableFeature(feature: string) { | ||
| return Promise.resolve(opts.msc4108Enabled && feature === "org.matrix.msc4108"); | ||
| }, | ||
| getUserId() { | ||
| return opts.userId; | ||
| }, | ||
| getDeviceId() { | ||
| return opts.deviceId; | ||
| }, | ||
| baseUrl, | ||
| getHomeserverUrl() { | ||
| return baseUrl; | ||
| }, | ||
| getDevice: jest.fn(), | ||
| getCrypto: jest.fn(() => crypto), | ||
| getAuthIssuer: jest.fn().mockResolvedValue({ issuer: "https://issuer/" }), | ||
| } as unknown as MatrixClient; | ||
| client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, { | ||
| baseUrl: client.baseUrl, | ||
| prefix: ClientPrefix.Unstable, | ||
| onlyData: true, | ||
| }); | ||
| return client; | ||
| } | ||
|
|
||
| describe("MSC4108SignInWithQR", () => { | ||
| beforeEach(() => { | ||
| fetchMock.get( | ||
| "https://issuer/.well-known/openid-configuration", | ||
| mockOpenIdConfiguration("https://issuer/", [DEVICE_CODE_SCOPE]), | ||
| ); | ||
| fetchMock.get("https://issuer/jwks", { | ||
| status: 200, | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| keys: [], | ||
| }); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| fetchMock.reset(); | ||
| }); | ||
|
|
||
| const url = "https://fallbackserver/rz/123"; | ||
|
|
||
| it("should generate qr code data as expected", async () => { | ||
| const session = new MSC4108RendezvousSession({ | ||
| url, | ||
| }); | ||
| const channel = new MSC4108SecureChannel(session); | ||
| const login = new MSC4108SignInWithQR(channel, false); | ||
|
|
||
| await login.generateCode(); | ||
| const code = login.code; | ||
| expect(code).toHaveLength(71); | ||
| const text = new TextDecoder().decode(code); | ||
| expect(text.startsWith("MATRIX")).toBeTruthy(); | ||
| expect(text.endsWith(url)).toBeTruthy(); | ||
|
|
||
| // Assert that the code is stable | ||
| await login.generateCode(); | ||
| expect(login.code).toEqual(code); | ||
| }); | ||
|
|
||
| describe("should be able to connect as a reciprocating device", () => { | ||
| let client: MatrixClient; | ||
| let ourLogin: MSC4108SignInWithQR; | ||
| let opponentLogin: MSC4108SignInWithQR; | ||
|
|
||
| beforeEach(async () => { | ||
| let ourData = defer<string>(); | ||
| let opponentData = defer<string>(); | ||
|
|
||
| const ourMockSession = { | ||
| send: jest.fn(async (newData) => { | ||
| ourData.resolve(newData); | ||
| }), | ||
| receive: jest.fn(() => { | ||
| const prom = opponentData.promise; | ||
| prom.then(() => { | ||
| opponentData = defer(); | ||
| }); | ||
| return prom; | ||
| }), | ||
| url, | ||
| cancelled: false, | ||
| cancel: () => { | ||
| // @ts-ignore | ||
| ourMockSession.cancelled = true; | ||
| ourData.resolve(""); | ||
| }, | ||
| } as unknown as MSC4108RendezvousSession; | ||
| const opponentMockSession = { | ||
| send: jest.fn(async (newData) => { | ||
| opponentData.resolve(newData); | ||
| }), | ||
| receive: jest.fn(() => { | ||
| const prom = ourData.promise; | ||
| prom.then(() => { | ||
| ourData = defer(); | ||
| }); | ||
| return prom; | ||
| }), | ||
| url, | ||
| } as unknown as MSC4108RendezvousSession; | ||
|
|
||
| client = makeMockClient({ userId: "@alice:example.com", deviceId: "alice", msc4108Enabled: true }); | ||
|
|
||
| const ourChannel = new MSC4108SecureChannel(ourMockSession); | ||
| const qrCodeData = QrCodeData.from_bytes( | ||
| await ourChannel.generateCode(QrCodeMode.Reciprocate, client.getHomeserverUrl()), | ||
| ); | ||
| const opponentChannel = new MSC4108SecureChannel(opponentMockSession, qrCodeData.public_key); | ||
|
|
||
| ourLogin = new MSC4108SignInWithQR(ourChannel, true, client); | ||
| opponentLogin = new MSC4108SignInWithQR(opponentChannel, false); | ||
| }); | ||
|
|
||
| it("should be able to connect with opponent and share homeserver url & check code", async () => { | ||
| await Promise.all([ | ||
| expect(ourLogin.negotiateProtocols()).resolves.toEqual({}), | ||
| expect(opponentLogin.negotiateProtocols()).resolves.toEqual({ homeserverBaseUrl: client.baseUrl }), | ||
| ]); | ||
|
|
||
| expect(ourLogin.checkCode).toBe(opponentLogin.checkCode); | ||
| }); | ||
|
|
||
| it("should be able to connect with opponent and share verificationUri", async () => { | ||
| await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]); | ||
|
|
||
| // We don't have the new device side of this flow implemented at this time so mock it | ||
| const deviceId = "DEADB33F"; | ||
| const verificationUri = "https://example.com/verify"; | ||
| const verificationUriComplete = "https://example.com/verify/complete"; | ||
|
|
||
| mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404)); | ||
|
|
||
| await Promise.all([ | ||
| expect(ourLogin.deviceAuthorizationGrant()).resolves.toEqual({ | ||
| verificationUri: verificationUriComplete, | ||
| }), | ||
| // @ts-ignore | ||
| opponentLogin.send({ | ||
| type: PayloadType.Protocol, | ||
| protocol: "device_authorization_grant", | ||
| device_authorization_grant: { | ||
| verification_uri: verificationUri, | ||
| verification_uri_complete: verificationUriComplete, | ||
| }, | ||
| device_id: deviceId, | ||
| }), | ||
| ]); | ||
| }); | ||
|
|
||
| it("should abort if device already exists", async () => { | ||
| await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]); | ||
|
|
||
| // We don't have the new device side of this flow implemented at this time so mock it | ||
| const deviceId = "DEADB33F"; | ||
| const verificationUri = "https://example.com/verify"; | ||
|
|
||
| mocked(client.getDevice).mockResolvedValue({} as IMyDevice); | ||
|
|
||
| await Promise.all([ | ||
| expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow("Specified device ID already exists"), | ||
| // @ts-ignore | ||
| opponentLogin.send({ | ||
| type: PayloadType.Protocol, | ||
| protocol: "device_authorization_grant", | ||
| device_authorization_grant: { | ||
| verification_uri: verificationUri, | ||
| }, | ||
| device_id: deviceId, | ||
| }), | ||
| ]); | ||
| }); | ||
|
|
||
| it("should abort on unsupported protocol", async () => { | ||
| await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]); | ||
|
|
||
| // We don't have the new device side of this flow implemented at this time so mock it | ||
| const deviceId = "DEADB33F"; | ||
| const verificationUri = "https://example.com/verify"; | ||
|
|
||
| await Promise.all([ | ||
| expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow( | ||
| "Received a request for an unsupported protocol", | ||
| ), | ||
| // @ts-ignore | ||
| opponentLogin.send({ | ||
| type: PayloadType.Protocol, | ||
| protocol: "device_authorization_grant_v2", | ||
| device_authorization_grant: { | ||
| verification_uri: verificationUri, | ||
| }, | ||
| device_id: deviceId, | ||
| }), | ||
| ]); | ||
| }); | ||
|
|
||
| it("should be able to connect with opponent and share secrets", async () => { | ||
| await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]); | ||
|
|
||
| // We don't have the new device side of this flow implemented at this time so mock it | ||
| // @ts-ignore | ||
| ourLogin.expectingNewDeviceId = "DEADB33F"; | ||
|
|
||
| const ourProm = ourLogin.shareSecrets(); | ||
|
|
||
| // Consume the ProtocolAccepted message which would normally be handled by step 4 which we do not have here | ||
| // @ts-ignore | ||
| await opponentLogin.receive(); | ||
|
|
||
| mocked(client.getDevice).mockResolvedValue({} as IMyDevice); | ||
|
|
||
| const secrets = { | ||
| cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" }, | ||
| }; | ||
| mocked(client.getCrypto()!.exportSecretsForQrLogin).mockResolvedValue(secrets); | ||
|
|
||
| const payload = { | ||
| secrets: expect.objectContaining(secrets), | ||
| }; | ||
| await Promise.all([ | ||
| expect(ourProm).resolves.toEqual(payload), | ||
| expect(opponentLogin.shareSecrets()).resolves.toEqual(payload), | ||
| ]); | ||
| }); | ||
|
|
||
| it("should abort if device doesn't come up by timeout", async () => { | ||
| jest.spyOn(global, "setTimeout").mockImplementation((fn) => { | ||
| (<Function>fn)(); | ||
| return -1; | ||
| }); | ||
| jest.spyOn(Date, "now").mockImplementation(() => { | ||
| return 12345678 + mocked(setTimeout).mock.calls.length * 1000; | ||
| }); | ||
|
|
||
| await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]); | ||
|
|
||
| // We don't have the new device side of this flow implemented at this time so mock it | ||
| // @ts-ignore | ||
| ourLogin.expectingNewDeviceId = "DEADB33F"; | ||
|
|
||
| // @ts-ignore | ||
| await opponentLogin.send({ | ||
| type: PayloadType.Success, | ||
| }); | ||
| mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404)); | ||
|
|
||
| const ourProm = ourLogin.shareSecrets(); | ||
| await expect(ourProm).rejects.toThrow("New device not found"); | ||
| }); | ||
|
|
||
| it("should abort on unexpected errors", async () => { | ||
| await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]); | ||
|
|
||
| // We don't have the new device side of this flow implemented at this time so mock it | ||
| // @ts-ignore | ||
| ourLogin.expectingNewDeviceId = "DEADB33F"; | ||
|
|
||
| // @ts-ignore | ||
| await opponentLogin.send({ | ||
| type: PayloadType.Success, | ||
| }); | ||
| mocked(client.getDevice).mockRejectedValue( | ||
| new MatrixError({ errcode: "M_UNKNOWN", error: "The message" }, 500), | ||
| ); | ||
|
|
||
| await expect(ourLogin.shareSecrets()).rejects.toThrow("The message"); | ||
| }); | ||
|
|
||
| it("should abort on declined login", async () => { | ||
| await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]); | ||
|
|
||
| await ourLogin.declineLoginOnExistingDevice(); | ||
| await expect(opponentLogin.shareSecrets()).rejects.toThrow("Unexpected message received"); | ||
| }); | ||
|
|
||
| it("should not send secrets if user cancels", async () => { | ||
| jest.spyOn(global, "setTimeout").mockImplementation((fn) => { | ||
| (<Function>fn)(); | ||
| return -1; | ||
| }); | ||
|
|
||
| await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]); | ||
|
|
||
| // We don't have the new device side of this flow implemented at this time so mock it | ||
| // @ts-ignore | ||
| ourLogin.expectingNewDeviceId = "DEADB33F"; | ||
|
|
||
| const ourProm = ourLogin.shareSecrets(); | ||
| const opponentProm = opponentLogin.shareSecrets(); | ||
|
|
||
| // Consume the ProtocolAccepted message which would normally be handled by step 4 which we do not have here | ||
| // @ts-ignore | ||
| await opponentLogin.receive(); | ||
|
|
||
| const deferred = defer<IMyDevice>(); | ||
| mocked(client.getDevice).mockReturnValue(deferred.promise); | ||
|
|
||
| ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {}); | ||
| deferred.resolve({} as IMyDevice); | ||
|
|
||
| const secrets = { | ||
| cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" }, | ||
| }; | ||
| mocked(client.getCrypto()!.exportSecretsForQrLogin).mockResolvedValue(secrets); | ||
|
|
||
| await Promise.all([ | ||
| expect(ourProm).rejects.toThrow("User cancelled"), | ||
| expect(opponentProm).rejects.toThrow("Unexpected message received"), | ||
| ]); | ||
| }); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.