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

Commit

Permalink
OIDC: Log in (#11199)
Browse files Browse the repository at this point in the history
* 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 <1389908+richvdh@users.noreply.github.com>

* 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 <1389908+richvdh@users.noreply.github.com>
  • Loading branch information
Kerry and richvdh committed Jul 11, 2023
1 parent 186497a commit 7b3d0ad
Show file tree
Hide file tree
Showing 7 changed files with 490 additions and 67 deletions.
92 changes: 91 additions & 1 deletion src/Lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020, 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.
Expand Down Expand Up @@ -65,6 +65,7 @@ import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLoc
import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload";
import { SdkContextClass } from "./contexts/SDKContext";
import { messageForLoginError } from "./utils/ErrorUtils";
import { completeOidcLogin } from "./utils/oidc/authorize";

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

/**
* If query string includes OIDC authorization code flow parameters attempt to login using oidc flow
* Else, we may be returning from SSO - attempt token login
*
* @param {Object} queryParams string->string map of the
* query-parameters extracted from the real query-string of the starting
* URI.
*
* @param {string} defaultDeviceDisplayName
* @param {string} fragmentAfterLogin path to go to after a successful login, only used for "Try again"
*
* @returns {Promise} promise which resolves to true if we completed the delegated auth login
* else false
*/
export async function attemptDelegatedAuthLogin(
queryParams: QueryDict,
defaultDeviceDisplayName?: string,
fragmentAfterLogin?: string,
): Promise<boolean> {
if (queryParams.code && queryParams.state) {
return attemptOidcNativeLogin(queryParams);
}

return attemptTokenLogin(queryParams, defaultDeviceDisplayName, fragmentAfterLogin);
}

/**
* Attempt to login by completing OIDC authorization code flow
* @param queryParams string->string map of the query-parameters extracted from the real query-string of the starting URI.
* @returns Promise that resolves to true when login succceeded, else false
*/
async function attemptOidcNativeLogin(queryParams: QueryDict): Promise<boolean> {
try {
const { accessToken, homeserverUrl, identityServerUrl } = await completeOidcLogin(queryParams);

const {
user_id: userId,
device_id: deviceId,
is_guest: isGuest,
} = await getUserIdFromAccessToken(accessToken, homeserverUrl, identityServerUrl);

const credentials = {
accessToken,
homeserverUrl,
identityServerUrl,
deviceId,
userId,
isGuest,
};

logger.debug("Logged in via OIDC native flow");
await onSuccessfulDelegatedAuthLogin(credentials);
return true;
} catch (error) {
logger.error("Failed to login via OIDC", error);

// TODO(kerrya) nice error messages https://github.com/vector-im/element-web/issues/25665
await onFailedDelegatedAuthLogin(_t("Something went wrong."));
return false;
}
}

/**
* Gets information about the owner of a given access token.
* @param accessToken
* @param homeserverUrl
* @param identityServerUrl
* @returns Promise that resolves with whoami response
* @throws when whoami request fails
*/
async function getUserIdFromAccessToken(
accessToken: string,
homeserverUrl: string,
identityServerUrl?: string,
): Promise<ReturnType<MatrixClient["whoami"]>> {
try {
const client = createClient({
baseUrl: homeserverUrl,
accessToken: accessToken,
idBaseUrl: identityServerUrl,
});

return await client.whoami();
} catch (error) {
logger.error("Failed to retrieve userId using accessToken", error);
throw new Error("Failed to retrieve userId using accessToken");
}
}

/**
* @param {QueryDict} queryParams string->string map of the
* query-parameters extracted from the real query-string of the starting
* URI.
*
* @param {string} defaultDeviceDisplayName
* @param {string} fragmentAfterLogin path to go to after a successful login, only used for "Try again"
*
* @returns {Promise} promise which resolves to true if we completed the token
* login, else false
*/
Expand Down
11 changes: 7 additions & 4 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,13 +316,17 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// the first thing to do is to try the token params in the query-string
// if the session isn't soft logged out (ie: is a clean session being logged in)
if (!Lifecycle.isSoftLogout()) {
Lifecycle.attemptTokenLogin(
Lifecycle.attemptDelegatedAuthLogin(
this.props.realQueryParams,
this.props.defaultDeviceDisplayName,
this.getFragmentAfterLogin(),
).then(async (loggedIn): Promise<boolean | void> => {
if (this.props.realQueryParams?.loginToken) {
// remove the loginToken from the URL regardless
if (
this.props.realQueryParams?.loginToken ||
this.props.realQueryParams?.code ||
this.props.realQueryParams?.state
) {
// remove the loginToken or auth code from the URL regardless
this.props.onTokenLoginCompleted();
}

Expand All @@ -341,7 +345,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// if the user has followed a login or register link, don't reanimate
// the old creds, but rather go straight to the relevant page
const firstScreen = this.screenAfterLogin ? this.screenAfterLogin.screen : null;

const restoreSuccess = await this.loadSession();
if (restoreSuccess) {
return true;
Expand Down
1 change: 1 addition & 0 deletions src/components/structures/auth/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
this.props.serverConfig.delegatedAuthentication!,
flow.clientId,
this.props.serverConfig.hsUrl,
this.props.serverConfig.isUrl,
);
}}
>
Expand Down
1 change: 1 addition & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"Failed to transfer call": "Failed to transfer call",
"Permission Required": "Permission Required",
"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",
"Something went wrong.": "Something went wrong.",
"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.",
"We couldn't log you in": "We couldn't log you in",
"Try again": "Try again",
Expand Down
44 changes: 44 additions & 0 deletions src/utils/oidc/authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { completeAuthorizationCodeGrant } from "matrix-js-sdk/src/oidc/authorize";
import { QueryDict } from "matrix-js-sdk/src/utils";
import { OidcClientConfig } from "matrix-js-sdk/src/autodiscovery";
import { generateOidcAuthorizationUrl } from "matrix-js-sdk/src/oidc/authorize";
import { randomString } from "matrix-js-sdk/src/randomstring";
Expand Down Expand Up @@ -49,3 +51,45 @@ export const startOidcLogin = async (

window.location.href = authorizationUrl;
};

/**
* Gets `code` and `state` query params
*
* @param queryParams
* @returns code and state
* @throws when code and state are not valid strings
*/
const getCodeAndStateFromQueryParams = (queryParams: QueryDict): { code: string; state: string } => {
const code = queryParams["code"];
const state = queryParams["state"];

if (!code || typeof code !== "string" || !state || typeof state !== "string") {
throw new Error("Invalid query parameters for OIDC native login. `code` and `state` are required.");
}
return { code, state };
};

/**
* Attempt to complete authorization code flow to get an access token
* @param queryParams the query-parameters extracted from the real query-string of the starting URI.
* @returns Promise that resolves with accessToken, identityServerUrl, and homeserverUrl when login was successful
* @throws When we failed to get a valid access token
*/
export const completeOidcLogin = async (
queryParams: QueryDict,
): Promise<{
homeserverUrl: string;
identityServerUrl?: string;
accessToken: string;
}> => {
const { code, state } = getCodeAndStateFromQueryParams(queryParams);
const { homeserverUrl, tokenResponse, identityServerUrl } = await completeAuthorizationCodeGrant(code, state);

// @TODO(kerrya) do something with the refresh token https://github.com/vector-im/element-web/issues/25444

return {
homeserverUrl: homeserverUrl,
identityServerUrl: identityServerUrl,
accessToken: tokenResponse.access_token,
};
};
Loading

0 comments on commit 7b3d0ad

Please sign in to comment.