-
Notifications
You must be signed in to change notification settings - Fork 47
BREAKING: implement Native OIDC as per MSC 3861 #2024
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
BREAKING: implement Native OIDC as per MSC 3861 #2024
Conversation
A comment on the changes to the database API: I initially intended to just keep all OIDC-related state (device ID, OAuth2.0 client ID, auth metadata) in memory until the OIDC flow completes. I sadly figured out the Dart process might get killed by the platform for battery optimization. In order to complete the flow after the main process was killed, I decided to store the OAuth2 client ID and the homeserver's auth metadata in the database as well as to split the device ID from the remaining client store since now the device ID is present a long time before the rest of the account is available. Even though I so far already implemented the database API changes with this in mind, there is no way to resume an OIDC flow from a new thread yet. |
Outdated review hintReview notice: So far, the default behavior of the updated EDIT: I adjusted the behavior of You can now easily use e.g. |
The < polycule > side implementation is now merged and works like a charm. Maybe that's helpful for testing this PR in real world: https://gitlab.com/polycule_client/polycule/-/merge_requests/245 |
Since I unaskedly opened this PR, I'd kindly offer to take care of the OIDC code's maintenance in future too. You folks all have my MXID and can always poke me if there are future issues about the OIDC code. |
ff732a4
to
83f4ec1
Compare
Clients might require the following workaround for #2027 : #2027 (comment) |
261932d
to
0ae4e30
Compare
55bd288
to
2732c27
Compare
2732c27
to
c483116
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any update on review of this PR ? It's now been months and months of waiting and I still did not even get a response on whether you are actually willing to ever accept this contribution. Instead only vaguely related comments.
c483116
to
895bb82
Compare
helo :3 sorry for the delay. I will now start reading the msc(s) and try to get back to this asap! (still expecting a marklin set on my bday!) edit:
Sorry for the delay, everyone internally has been super busy with "work". I have not heard anything about not accepting this contribution so I will review it with the end goal of getting it merged asap! Again thank you very much for the contribution and sorry for the delay! |
9ad6df5
to
0c90f2b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very draft review, will try to go through the whole flow and look at the client implementation after this
@@ -163,7 +163,6 @@ void main() { | |||
now, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it be possible to add tests for this?
refreshTokenLifetimeMs: customRefreshTokenLifetime?.inMilliseconds, | ||
); | ||
|
||
this.accessToken = accessToken = tokenResponse.accessToken; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick repeated bit, would it be possible to deduplicate it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't deduplicate this without adding a !
null check we can avoid. Better variables duplicated than unsound null safety.
@@ -0,0 +1,44 @@ | |||
import 'package:matrix/matrix.dart'; | |||
|
|||
extension Msc4191AccountManagementExtension on Client { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
only a little bit concerned that this is a draft msc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's a draft but a) it doesn't break anything and b) it's so much of a simple MSC, I don't see much regression in it - even if we should happen to adjust the behavior later in time.
}) { | ||
return OidcDynamicRegistrationData( | ||
clientName: { | ||
null: defaultLocale.clientName, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick but having a map key as null doesn't really look right. Would {clientName: locale?} be better here? A little wrapper class would probably be a bit cleaner
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmmm, a null key in the map is sadly exactly what the Dart Uir
class expects for its queryParameters
property. I do not see need for introducing a new helper class just in order to later map it to a Uri.queryParameters
instance again just because the syntax of this parameter is a bit weird. What would be the benefit for end users ? They won't touch this part of the code anyway since they only provide the parameters the SDK will later write into the Map
.
authEndpoint = Uri.parse(authData['authorization_endpoint'] as String); | ||
tokenEndpoint = Uri.parse(authData['token_endpoint'] as String); | ||
// ensure we only hand over permitted prompts | ||
if (prompt != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the prompt doesn't match, do we want to continue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. A client might request a prompt the server does not know. Using this handler here, the authentication server will fall back on the default selection showing all possible ways to proceed. The only behavior we need to avoid is passing an invalid prompt since this would cause the authentication server to show an error screen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(E.g. a client could hard-code the two prompts login
and register
. In this case the SDK takes care of not prompting an invalid option to the MAS if e.g. registration is disabled.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact, it's a pure convenience feature making client implementation easier and more reliable.
'state': state, | ||
if (prompt != null) 'prompt': prompt, | ||
'code_challenge': | ||
// remove the "=" padding |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make sure to remove ALL the padding?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dart's base64
codec only adds the trailing =
as padding, doesn't it ? Any suggestions how to better handle the padding ? I'm a bit clueless on how to do this without over engineering the implementation. Help appreciated.
Implements: - MSC 3861 - Next-generation auth for Matrix, based on OAuth 2.0/OIDC - MSC 1597 - Better spec for matrix identifiers - MSC 2964 - Usage of OAuth 2.0 authorization code grant and refresh token grant - MSC 2965 - OAuth 2.0 Authorization Server Metadata discovery - MSC 2966 - Usage of OAuth 2.0 Dynamic Client Registration in Matrix - MSC 2967 - API scopes - MSC 3824 - OIDC aware clients - MSC 4191 - Account management deep-linking Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
…i to client Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
Signed-off-by: The one with the braid <info@braid.business>
f287859
to
48bb3b3
Compare
|
||
Future<void> storeOidcDynamicClientId(String? oidcClientId); | ||
|
||
Future<String?> getOidcDynamicClientId(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need this. Just use getClient()
and pick what you need from it. To have it type safe we could later migrate it to returning a Dart Record.
Yeah that would fetch more data than necessary but I would prefer this over having the database API even more complex.
/// MSC 2964 & MSC 2967 | ||
Future<String> oidcEnsureDeviceId([bool enforceNewDevice = false]) async { | ||
if (!enforceNewDevice) { | ||
final storedDeviceId = await database?.getDeviceId(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need the stored device ID and can't just use Client.deviceID
? It should be the same
|
||
extension GenerateDeviceIdExtension on Client { | ||
/// MSC 2964 & MSC 2967 | ||
Future<String> oidcEnsureDeviceId([bool enforceNewDevice = false]) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add more comments what this does? The method name is super unintuitive. Links to the MSCs would also be more helpful than MSC numbers.
import 'package:matrix/src/utils/crypto/crypto.dart'; | ||
|
||
extension OidcOauthGrantFlowExtension on Client { | ||
Future<void> oidcAuthorizationGrantFlow({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method name says nothing about what it does? No idea what this is flow. Please switch this to a proper name with dart docs
@@ -2134,6 +2194,13 @@ class Client extends MatrixApi { | |||
_discoveryDataLoading = database.getWellKnown().then((data) { | |||
_wellKnown = data; | |||
}); | |||
_oidcAuthMetadataLoading = database.getOidcAuthMetadata().then((data) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I doubt that this really needs to be waited for. Can't we just make it part of the Database.getClient
call?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks quite good so far, though I'm not that into the MSCs of Matrix. Will test out if it works with oidc login on FluffyChat soon...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add an example how to actually use this?
BREAKING: changes to the database API to better reflect the OIDC state
This implementation is based on the draft of native OIDC I implemented in < polycule >. It can be found here. Unlike in my initial draft, this implementation of course does not rely on any Flutter package but ships a minimal OIDC state machine relying on the matrix client to provide the native callback result via a
Completer
.This implementation does not aim to comply to the full OIDC standard but only the subset required as per the MSCs stated below.
This PR does not aim to implement OIDC device flow as per MSC 4108 nor the changes for the application services as per MSC 4190.
Roadmap:
Implements: