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

[WIP] OIDC: Login #11093

Closed
wants to merge 86 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
c2a78d1
add delegatedauthentication to validated server config
Jun 7, 2023
f946b85
dynamic client registration functions
Jun 9, 2023
223d3ff
Merge branch 'develop' into kerry/25466/oidc-validate-wk-mauthenticat…
Jun 11, 2023
f83499e
Merge branch 'kerry/25466/oidc-validate-wk-mauthentication-2' into ke…
Jun 11, 2023
4201725
test OP registration functions
Jun 12, 2023
314105d
add stubbed nativeOidc flow setup in Login
Jun 12, 2023
6fe2d35
cover more error cases in Login
Jun 12, 2023
c473f11
Merge branch 'kerry/25468/test-login' into kerry/25468/oidc-client-re…
Jun 12, 2023
19dc425
tidy
Jun 12, 2023
94d5c36
test dynamic client registration in Login
Jun 12, 2023
67f6c2e
comment oidc_static_clients
Jun 12, 2023
8df0929
register oidc inside Login.getFlows
Jun 12, 2023
0d4de13
Merge branch 'develop' into kerry/25466/oidc-validate-wk-mauthenticat…
Jun 12, 2023
1a53ad0
strict fixes
Jun 12, 2023
f9aaa28
remove unused code
Jun 12, 2023
9faa9c8
and imports
Jun 12, 2023
0f76ffa
Merge branch 'kerry/25466/oidc-validate-wk-mauthentication-2' into ke…
Jun 13, 2023
4dfa18d
Merge branch 'develop' into kerry/25468/oidc-client-registration
Jun 13, 2023
e920229
comments
Jun 13, 2023
5389d5c
comments 2
Jun 13, 2023
af63a90
util functions to get static client id
Jun 13, 2023
9c89f41
check static client ids in login flow
Jun 13, 2023
056a713
Merge branch 'kerry/25468/oidc-client-static-reg' into kerry/25468/oi…
Jun 13, 2023
422823e
remove dead code
Jun 14, 2023
8483b2f
Merge branch 'kerry/25468/oidc-client-static-reg' into kerry/25468/oi…
Jun 14, 2023
a76cde9
OidcRegistrationClientMetadata type
Jun 14, 2023
bf45be2
navigate to oidc authorize url
Jun 14, 2023
e1e22f3
Merge branch 'develop' into kerry/25468/oidc-client-static-reg
Jun 14, 2023
f4a2ff2
Merge branch 'kerry/25468/oidc-client-static-reg' into kerry/25468/oi…
Jun 14, 2023
3b8938c
exchange code for token
Jun 15, 2023
52d03c2
navigate to oidc authorize url
Jun 14, 2023
3c499da
navigate to oidc authorize url
Jun 15, 2023
551670f
test
Jun 15, 2023
423af53
Merge branch 'develop' into kerry/25574/oidc-authorization-endpoint
Jun 22, 2023
b100d6d
adjust for js-sdk code
Jun 23, 2023
6c1321f
Merge branch 'kerry/25574/oidc-authorization-endpoint' into kerry/255…
Jun 23, 2023
28a36ec
login with oidc native flow: messy version
Jun 23, 2023
a1b975d
tidy
Jun 23, 2023
72ff46a
update test for response_mode query
Jun 23, 2023
f566a60
Merge branch 'kerry/25574/oidc-authorization-endpoint' into kerry/255…
Jun 23, 2023
dd9944d
tidy up some TODOs
Jun 23, 2023
857df8d
use new types
Jun 26, 2023
d0ce2a2
Merge branch 'kerry/25574/oidc-authorization-endpoint' into kerry/255…
Jun 26, 2023
9510727
add identityServerUrl to stored params
Jun 26, 2023
044beb7
unit test completeOidcLogin
Jun 26, 2023
f4ff6f5
test tokenlogin
Jun 27, 2023
dbd54c5
Merge branch 'develop' into kerry/25574/oidc-authorization-endpoint
Jun 27, 2023
599baa2
strict
Jun 27, 2023
637eecc
Merge branch 'kerry/25574/oidc-authorization-endpoint' into kerry/255…
Jun 27, 2023
905bac0
whitespace
Jun 27, 2023
7c91acd
tidy
Jun 27, 2023
3dff562
Merge branch 'develop' into kerry/25574/test-token-login
Jun 27, 2023
bb2c50b
Merge branch 'kerry/25574/test-token-login' into kerry/25574/oidc-aut…
Jun 27, 2023
5ba262c
unit test oidc login flow in MatrixChat
Jun 27, 2023
94167e5
strict
Jun 27, 2023
0463d25
Merge branch 'kerry/25574/test-token-login' into kerry/25574/oidc-aut…
Jun 27, 2023
ffbc140
Merge branch 'develop' into kerry/25574/oidc-authorization-endpoint
Jun 28, 2023
88e0cf2
tidy
Jun 28, 2023
480da01
Merge branch 'kerry/25574/oidc-authorization-endpoint' into kerry/255…
Jun 28, 2023
26a745f
extract success/failure handlers from token login function
Jun 28, 2023
38ecd44
typo
Jun 28, 2023
046a4a4
Merge branch 'kerry/25574/token-login-refactor' into kerry/25574/oidc…
Jun 28, 2023
4890e66
use for no homeserver error dialog too
Jun 28, 2023
1a59a7b
Merge branch 'kerry/25574/token-login-refactor' into kerry/25574/oidc…
Jun 28, 2023
225e4f5
reuse post-token login functions, test
Jun 28, 2023
c7199e5
shuffle testing utils around
Jun 28, 2023
56e93ae
shuffle testing utils around
Jun 28, 2023
b9792bc
i18n
Jun 28, 2023
a9ed791
tidy
Jun 28, 2023
71f1633
Update src/Lifecycle.ts
Jun 28, 2023
c920e45
Merge branch 'develop' into kerry/25574/token-login-refactor
Jun 28, 2023
fdfaa01
Merge branch 'develop' into kerry/25574/token-login-refactor
Jun 29, 2023
3602b65
Merge branch 'develop' into kerry/25574/oidc-authorization
Jun 29, 2023
5fddb23
Merge branch 'kerry/25574/token-login-refactor' into kerry/25574/oidc…
Jun 29, 2023
11a6ccc
tidy
Jun 29, 2023
5a2aaad
Merge branch 'kerry/25574/token-login-refactor' into kerry/25574/oidc…
Jun 29, 2023
a7d4e44
comment
Jun 29, 2023
e389fde
update tests for id token validation
Jun 29, 2023
cb8832c
move try again responsibility
Jun 29, 2023
9dccd7a
Merge branch 'kerry/25574/token-login-refactor' into kerry/25574/oidc…
Jun 30, 2023
6ae1965
Merge branch 'develop' into kerry/25574/oidc-authorization
Jul 3, 2023
ccadb29
prettier
Jul 3, 2023
2afdaf4
use more future proof config for static clients
Jul 3, 2023
b02e525
Merge branch 'develop' into kerry/oidc-static-client-config
Jul 3, 2023
6cbe9a1
Merge branch 'kerry/oidc-static-client-config' into kerry/25574/oidc-…
Jul 4, 2023
516800a
Merge branch 'develop' into kerry/25574/oidc-authorization
Jul 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
107 changes: 101 additions & 6 deletions src/utils/oidc/authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import {
AuthorizationParams,
generateAuthorizationParams,
generateAuthorizationUrl,
completeAuthorizationCodeGrant,
} from "matrix-js-sdk/src/oidc/authorize";
import { QueryDict } from "matrix-js-sdk/src/utils";

import { ValidatedDelegatedAuthConfig } from "../ValidatedServerConfig";

Expand All @@ -31,16 +33,65 @@ import { ValidatedDelegatedAuthConfig } from "../ValidatedServerConfig";
*/
const storeAuthorizationParams = (
{ redirectUri, state, nonce, codeVerifier }: AuthorizationParams,
{ issuer }: ValidatedDelegatedAuthConfig,
delegatedAuthConfig: ValidatedDelegatedAuthConfig,
clientId: string,
homeserver: string,
homeserverUrl: string,
identityServerUrl?: string,
): void => {
window.sessionStorage.setItem(`oidc_${state}_nonce`, nonce);
window.sessionStorage.setItem(`oidc_${state}_redirectUri`, redirectUri);
window.sessionStorage.setItem(`oidc_${state}_codeVerifier`, codeVerifier);
window.sessionStorage.setItem(`oidc_${state}_clientId`, clientId);
window.sessionStorage.setItem(`oidc_${state}_issuer`, issuer);
window.sessionStorage.setItem(`oidc_${state}_homeserver`, homeserver);
window.sessionStorage.setItem(`oidc_${state}_delegatedAuthConfig`, JSON.stringify(delegatedAuthConfig));
window.sessionStorage.setItem(`oidc_${state}_homeserverUrl`, homeserverUrl);
if (identityServerUrl) {
window.sessionStorage.setItem(`oidc_${state}_identityServerUrl`, identityServerUrl);
}
};

type StoredAuthorizationParams = Omit<ReturnType<typeof generateAuthorizationParams>, "state" | "scope"> & {
delegatedAuthConfig: ValidatedDelegatedAuthConfig;
clientId: string;
homeserverUrl: string;
identityServerUrl: string;
};

/**
* Validate that stored params are present and valid
* @param params as retrieved from session storage
* @returns validated stored authorization params
* @throws when params are invalid or missing
*/
const validateStoredAuthorizationParams = (params: Partial<StoredAuthorizationParams>): StoredAuthorizationParams => {
const requiredStringProperties = ["nonce", "redirectUri", "codeVerifier", "clientId", "homeserverUrl"];
if (
requiredStringProperties.every((key: string) => params[key] && typeof params[key] === "string") &&
(params.identityServerUrl === undefined || typeof params.identityServerUrl === "string") &&
!!params.delegatedAuthConfig
) {
return params as StoredAuthorizationParams;
}
throw new Error("Cannot complete OIDC login: required properties not found in session storage");
};

const retrieveAuthorizationParams = (state: string): StoredAuthorizationParams => {
const nonce = window.sessionStorage.getItem(`oidc_${state}_nonce`);
const redirectUri = window.sessionStorage.getItem(`oidc_${state}_redirectUri`);
const codeVerifier = window.sessionStorage.getItem(`oidc_${state}_codeVerifier`);
const clientId = window.sessionStorage.getItem(`oidc_${state}_clientId`);
const homeserverUrl = window.sessionStorage.getItem(`oidc_${state}_homeserverUrl`);
const identityServerUrl = window.sessionStorage.getItem(`oidc_${state}_identityServerUrl`) ?? undefined;
const delegatedAuthConfig = window.sessionStorage.getItem(`oidc_${state}_delegatedAuthConfig`);

return validateStoredAuthorizationParams({
nonce,
redirectUri,
codeVerifier,
clientId,
homeserverUrl,
identityServerUrl,
delegatedAuthConfig: delegatedAuthConfig ? JSON.parse(delegatedAuthConfig) : undefined,
});
};

/**
Expand All @@ -55,13 +106,14 @@ const storeAuthorizationParams = (
export const startOidcLogin = async (
delegatedAuthConfig: ValidatedDelegatedAuthConfig,
clientId: string,
homeserver: string,
homeserverUrl: string,
identityServerUrl?: string,
): Promise<void> => {
// TODO(kerrya) afterloginfragment https://github.com/vector-im/element-web/issues/25656
const redirectUri = window.location.origin;
const authParams = generateAuthorizationParams({ redirectUri });

storeAuthorizationParams(authParams, delegatedAuthConfig, clientId, homeserver);
storeAuthorizationParams(authParams, delegatedAuthConfig, clientId, homeserverUrl, identityServerUrl);

const authorizationUrl = await generateAuthorizationUrl(
delegatedAuthConfig.authorizationEndpoint,
Expand All @@ -71,3 +123,46 @@ 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 storedAuthorizationParams = retrieveAuthorizationParams(state);

const bearerTokenResponse = await completeAuthorizationCodeGrant(code, storedAuthorizationParams);
// @TODO(kerrya) do something with the refresh token https://github.com/vector-im/element-web/issues/25444
return {
homeserverUrl: storedAuthorizationParams.homeserverUrl,
identityServerUrl: storedAuthorizationParams.identityServerUrl,
accessToken: bearerTokenResponse.access_token,
};
};
Loading