-
Notifications
You must be signed in to change notification settings - Fork 57
Description
Preflight Checklist
- I could not find a solution in the existing issues, docs, nor discussions
Describe your problem
I want to use MSC3861 easily with the Matrix Dart SDK.
Describe your ideal solution
To fully implement OIDC as per MSC3861 we need to support:
- MSC2964: Usage of OAuth 2.0 authorization code grant and refresh token grant matrix-org/matrix-spec-proposals#2964 describes how the main authentication flow works
- MSC2965: OAuth 2.0 Authorization Server Metadata discovery matrix-org/matrix-spec-proposals#2965 describes how a client can discover the authentication server metadata of the homeserver
- MSC2966: Usage of OAuth 2.0 Dynamic Client Registration in Matrix matrix-org/matrix-spec-proposals#2966 describes how a client can register itself with the homeserver to get a client identifier
- MSC2967: API scopes matrix-org/matrix-spec-proposals#2967 defines the first set of access scopes and the basis for future access scopes
- MSC4254: Usage of RFC7009 Token Revocation for Matrix client logout matrix-org/matrix-spec-proposals#4254 describes how a client can end a client session
MSC 2965 discover authentication server metadata
First we need to discover the auth metadata. We should do this as a step of Client.checkHomeserver() because there we already discover other login-related information from the server. Client.checkHomeserver() already returns a record of multiple objects. To support MSC 2965 it should also return an object of type GetAuthMetadataResponse. This can be fetched by using the method Client.getAuthMetadata() if the requirements are fulfilled (Matrix Spec version 1.16 is supported).
MSC 2966 Dynamically Register OIDC Client
We should implement a new method Future<OidcClientData> Client.registerOidcClient({required GetAuthMetadataResponse authMetadata, String? clientName,}) which takes the registrationEndpoint from GetAuthMetadataResponse and returns an Object with all necessary data. The client should not yet store anything and leave the persistancy up to the SDK consumer if desired for this step which is unlikely.
MSC 2964 OIDC Login Flow
To fully support all cases we should implement two methods here:
({Uri authentiationUrl, String codeVerifier}) Client.initOidcLoginFlow({required GetAuthMetadataResponse authMetadata, required OidcClientData oidcClientData, required Uri redirectUri, Set<String>? scopes});
Future<void> Client.oidcLogin({required GetAuthMetadataResponse authMetadata, required OidcClientData oidcClientData, required String code, required String codeVerifier});Additional client information to store:
In the Client.init() method the user would have to store some additional information we might need later for authenticing for Crypto reset or logout like the oidc client ID. We should not store GetAuthMetadataResponse which we can request again at any time.
Example Flow
To fully login the SDK consumer would do something like the following:
final client = Client(/*...*/);
final (_,_,_,authMetadata) = await client.checkHomeserver(Uri.https('matrix.org'));
if (authMetadata == null) return; // Server does not support OIDC
final oidcClientData = await client.registerOidcClient(authMetadata: authMetadata, Uri redirectUri, /* ... */);
final (authenticationUrl, codeVerifier) = client.initOidcLoginFlow(
authMetadata: authMetadata,
oidcClientData: oidcClientData, // Contains the redirect uri
);
final code = await FlutterWebAuth2.authenticate( // Or any other way to open a browser and get the code
url: authenticationUrl,
);
await client.oidcLogin(
authMetadata: authMetadata,
oidcClientData: oidcClientData,
code: code,
codeVerifier: codeVerifier,
);
// And we are logged in!Logout
We should store the client_id in the same way we store deviceID and userID.
Once we are doing this we should check in the Client.logout() method, if the client_id is not null.
If it is not null, we assume that we have used OIDC for login.
We call Client.getAuthMetadata() to fetch the revoke endpoint.
We need a new methode Client.revokeToken(String token, String tokenTypeHint) which calls the revoke endpoint and sends clientID, token, and tokenTypeHint (which is access_token or refresh_token) to the revoke endpoint.
We call this method in the Client.logout() method instead of super.logout() and pass in our access token.
If the refresh token is not null we call the endpoint again for the refresh token.
Refresh Token
Similar to logout, if client_id is not null, we fetch Client.getAuthMetadata() (we should cache it in memory) and use the refresh endpoint there instead the one from the Matrix spec.
Reset Crypto Identity
This needs to be done in the Client but is not that complicated. An example can be found here: krille-chan/fluffychat@20dc024
Version
No response
Security requirements
No response
Additional Context
A previous implementation from an external contributor tried to implement it by storing a lot of additional state and unifying two methods into one client.oidcAuthorizationGrantFlow() which masks a lot of the complexity. However it makes the flow actually much more complex by adding a Completer() into the game and making it impossible to use the flow with a session reload in between which is the default case for web browsers which perform the login flow in the same tab, not a new one (which is a much better UX). The approach described in this issue also avoids unnecessary state in is therefore more stable and easier to test.