Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 7b3d0ad

Browse files
Kerryrichvdh
andauthored
OIDC: Log in (#11199)
* add delegatedauthentication to validated server config * dynamic client registration functions * test OP registration functions * add stubbed nativeOidc flow setup in Login * cover more error cases in Login * tidy * test dynamic client registration in Login * comment oidc_static_clients * register oidc inside Login.getFlows * strict fixes * remove unused code * and imports * comments * comments 2 * util functions to get static client id * check static client ids in login flow * remove dead code * OidcRegistrationClientMetadata type * navigate to oidc authorize url * exchange code for token * navigate to oidc authorize url * navigate to oidc authorize url * test * adjust for js-sdk code * login with oidc native flow: messy version * tidy * update test for response_mode query * tidy up some TODOs * use new types * add identityServerUrl to stored params * unit test completeOidcLogin * test tokenlogin * strict * whitespace * tidy * unit test oidc login flow in MatrixChat * strict * tidy * extract success/failure handlers from token login function * typo * use for no homeserver error dialog too * reuse post-token login functions, test * shuffle testing utils around * shuffle testing utils around * i18n * tidy * Update src/Lifecycle.ts Co-authored-by: Richard van der Hoff <[email protected]> * tidy * comment * update tests for id token validation * move try again responsibility * prettier * use more future proof config for static clients * test util for oidcclientconfigs * rename type and lint * correct oidc test util * store issuer and clientId pre auth navigation * adjust for js-sdk changes * update for js-sdk userstate, tidy * update MatrixChat tests * update tests --------- Co-authored-by: Richard van der Hoff <[email protected]>
1 parent 186497a commit 7b3d0ad

File tree

7 files changed

+490
-67
lines changed

7 files changed

+490
-67
lines changed

src/Lifecycle.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Copyright 2015, 2016 OpenMarket Ltd
33
Copyright 2017 Vector Creations Ltd
44
Copyright 2018 New Vector Ltd
5-
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
5+
Copyright 2019, 2020, 2023 The Matrix.org Foundation C.I.C.
66
77
Licensed under the Apache License, Version 2.0 (the "License");
88
you may not use this file except in compliance with the License.
@@ -65,6 +65,7 @@ import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLoc
6565
import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload";
6666
import { SdkContextClass } from "./contexts/SDKContext";
6767
import { messageForLoginError } from "./utils/ErrorUtils";
68+
import { completeOidcLogin } from "./utils/oidc/authorize";
6869

6970
const HOMESERVER_URL_KEY = "mx_hs_url";
7071
const ID_SERVER_URL_KEY = "mx_is_url";
@@ -182,13 +183,102 @@ export async function getStoredSessionOwner(): Promise<[string, boolean] | [null
182183
}
183184

184185
/**
186+
* If query string includes OIDC authorization code flow parameters attempt to login using oidc flow
187+
* Else, we may be returning from SSO - attempt token login
188+
*
185189
* @param {Object} queryParams string->string map of the
186190
* query-parameters extracted from the real query-string of the starting
187191
* URI.
188192
*
189193
* @param {string} defaultDeviceDisplayName
190194
* @param {string} fragmentAfterLogin path to go to after a successful login, only used for "Try again"
191195
*
196+
* @returns {Promise} promise which resolves to true if we completed the delegated auth login
197+
* else false
198+
*/
199+
export async function attemptDelegatedAuthLogin(
200+
queryParams: QueryDict,
201+
defaultDeviceDisplayName?: string,
202+
fragmentAfterLogin?: string,
203+
): Promise<boolean> {
204+
if (queryParams.code && queryParams.state) {
205+
return attemptOidcNativeLogin(queryParams);
206+
}
207+
208+
return attemptTokenLogin(queryParams, defaultDeviceDisplayName, fragmentAfterLogin);
209+
}
210+
211+
/**
212+
* Attempt to login by completing OIDC authorization code flow
213+
* @param queryParams string->string map of the query-parameters extracted from the real query-string of the starting URI.
214+
* @returns Promise that resolves to true when login succceeded, else false
215+
*/
216+
async function attemptOidcNativeLogin(queryParams: QueryDict): Promise<boolean> {
217+
try {
218+
const { accessToken, homeserverUrl, identityServerUrl } = await completeOidcLogin(queryParams);
219+
220+
const {
221+
user_id: userId,
222+
device_id: deviceId,
223+
is_guest: isGuest,
224+
} = await getUserIdFromAccessToken(accessToken, homeserverUrl, identityServerUrl);
225+
226+
const credentials = {
227+
accessToken,
228+
homeserverUrl,
229+
identityServerUrl,
230+
deviceId,
231+
userId,
232+
isGuest,
233+
};
234+
235+
logger.debug("Logged in via OIDC native flow");
236+
await onSuccessfulDelegatedAuthLogin(credentials);
237+
return true;
238+
} catch (error) {
239+
logger.error("Failed to login via OIDC", error);
240+
241+
// TODO(kerrya) nice error messages https://github.com/vector-im/element-web/issues/25665
242+
await onFailedDelegatedAuthLogin(_t("Something went wrong."));
243+
return false;
244+
}
245+
}
246+
247+
/**
248+
* Gets information about the owner of a given access token.
249+
* @param accessToken
250+
* @param homeserverUrl
251+
* @param identityServerUrl
252+
* @returns Promise that resolves with whoami response
253+
* @throws when whoami request fails
254+
*/
255+
async function getUserIdFromAccessToken(
256+
accessToken: string,
257+
homeserverUrl: string,
258+
identityServerUrl?: string,
259+
): Promise<ReturnType<MatrixClient["whoami"]>> {
260+
try {
261+
const client = createClient({
262+
baseUrl: homeserverUrl,
263+
accessToken: accessToken,
264+
idBaseUrl: identityServerUrl,
265+
});
266+
267+
return await client.whoami();
268+
} catch (error) {
269+
logger.error("Failed to retrieve userId using accessToken", error);
270+
throw new Error("Failed to retrieve userId using accessToken");
271+
}
272+
}
273+
274+
/**
275+
* @param {QueryDict} queryParams string->string map of the
276+
* query-parameters extracted from the real query-string of the starting
277+
* URI.
278+
*
279+
* @param {string} defaultDeviceDisplayName
280+
* @param {string} fragmentAfterLogin path to go to after a successful login, only used for "Try again"
281+
*
192282
* @returns {Promise} promise which resolves to true if we completed the token
193283
* login, else false
194284
*/

src/components/structures/MatrixChat.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,13 +316,17 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
316316
// the first thing to do is to try the token params in the query-string
317317
// if the session isn't soft logged out (ie: is a clean session being logged in)
318318
if (!Lifecycle.isSoftLogout()) {
319-
Lifecycle.attemptTokenLogin(
319+
Lifecycle.attemptDelegatedAuthLogin(
320320
this.props.realQueryParams,
321321
this.props.defaultDeviceDisplayName,
322322
this.getFragmentAfterLogin(),
323323
).then(async (loggedIn): Promise<boolean | void> => {
324-
if (this.props.realQueryParams?.loginToken) {
325-
// remove the loginToken from the URL regardless
324+
if (
325+
this.props.realQueryParams?.loginToken ||
326+
this.props.realQueryParams?.code ||
327+
this.props.realQueryParams?.state
328+
) {
329+
// remove the loginToken or auth code from the URL regardless
326330
this.props.onTokenLoginCompleted();
327331
}
328332

@@ -341,7 +345,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
341345
// if the user has followed a login or register link, don't reanimate
342346
// the old creds, but rather go straight to the relevant page
343347
const firstScreen = this.screenAfterLogin ? this.screenAfterLogin.screen : null;
344-
345348
const restoreSuccess = await this.loadSession();
346349
if (restoreSuccess) {
347350
return true;

src/components/structures/auth/Login.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
477477
this.props.serverConfig.delegatedAuthentication!,
478478
flow.clientId,
479479
this.props.serverConfig.hsUrl,
480+
this.props.serverConfig.isUrl,
480481
);
481482
}}
482483
>

src/i18n/strings/en_EN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"Failed to transfer call": "Failed to transfer call",
102102
"Permission Required": "Permission Required",
103103
"You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
104+
"Something went wrong.": "Something went wrong.",
104105
"We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.",
105106
"We couldn't log you in": "We couldn't log you in",
106107
"Try again": "Try again",

src/utils/oidc/authorize.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import { completeAuthorizationCodeGrant } from "matrix-js-sdk/src/oidc/authorize";
18+
import { QueryDict } from "matrix-js-sdk/src/utils";
1719
import { OidcClientConfig } from "matrix-js-sdk/src/autodiscovery";
1820
import { generateOidcAuthorizationUrl } from "matrix-js-sdk/src/oidc/authorize";
1921
import { randomString } from "matrix-js-sdk/src/randomstring";
@@ -49,3 +51,45 @@ export const startOidcLogin = async (
4951

5052
window.location.href = authorizationUrl;
5153
};
54+
55+
/**
56+
* Gets `code` and `state` query params
57+
*
58+
* @param queryParams
59+
* @returns code and state
60+
* @throws when code and state are not valid strings
61+
*/
62+
const getCodeAndStateFromQueryParams = (queryParams: QueryDict): { code: string; state: string } => {
63+
const code = queryParams["code"];
64+
const state = queryParams["state"];
65+
66+
if (!code || typeof code !== "string" || !state || typeof state !== "string") {
67+
throw new Error("Invalid query parameters for OIDC native login. `code` and `state` are required.");
68+
}
69+
return { code, state };
70+
};
71+
72+
/**
73+
* Attempt to complete authorization code flow to get an access token
74+
* @param queryParams the query-parameters extracted from the real query-string of the starting URI.
75+
* @returns Promise that resolves with accessToken, identityServerUrl, and homeserverUrl when login was successful
76+
* @throws When we failed to get a valid access token
77+
*/
78+
export const completeOidcLogin = async (
79+
queryParams: QueryDict,
80+
): Promise<{
81+
homeserverUrl: string;
82+
identityServerUrl?: string;
83+
accessToken: string;
84+
}> => {
85+
const { code, state } = getCodeAndStateFromQueryParams(queryParams);
86+
const { homeserverUrl, tokenResponse, identityServerUrl } = await completeAuthorizationCodeGrant(code, state);
87+
88+
// @TODO(kerrya) do something with the refresh token https://github.com/vector-im/element-web/issues/25444
89+
90+
return {
91+
homeserverUrl: homeserverUrl,
92+
identityServerUrl: identityServerUrl,
93+
accessToken: tokenResponse.access_token,
94+
};
95+
};

0 commit comments

Comments
 (0)