Skip to content

Implement MSC3861 OIDC #2176

@krille-chan

Description

@krille-chan

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:

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions