Draft: OIDC native login#660
Conversation
There was a problem hiding this comment.
Thank you for your PR! Great start, while it still seems a bit WIP. I like the observableValue.map()! I had a first look and left some comments. In a few places it seems to deviate a bit from how things are doing for SSO, which is probably not intentional, unless I'm missing something?
| this._authorizationEndpoint = null; | ||
| this._api = new OidcApi({ | ||
| clientId: "hydrogen-web", | ||
| issuer: options.loginOptions.oidc.issuer, |
There was a problem hiding this comment.
consider only passing in the oidc login method here, as you don't need the other ones?
There was a problem hiding this comment.
I did it this way so it is consistent with StartSSOLoginViewModel
| ): Promise<string> { | ||
| const encoder = new TextEncoder(); | ||
| const data = encoder.encode(codeVerifier); | ||
| const digest = await window.crypto.subtle.digest("SHA-256", data); |
There was a problem hiding this comment.
you can use platform.crypto.digest("SHA-256", data) here, which deals better with cross-browser differences and maybe one day different platforms.
| async _generateCodeChallenge( | ||
| codeVerifier: string | ||
| ): Promise<string> { | ||
| const encoder = new TextEncoder(); |
There was a problem hiding this comment.
You can use platform.encoding.utf8.encode() here, which deals better with cross-browser differences and maybe one day different platforms.
| } | ||
|
|
||
| get redirectUri() { | ||
| return window.location.origin; |
There was a problem hiding this comment.
We generally don't use DOM apis like window in any code not in platform/web, see startSSOLogin in StartSSOLoginViewModel how to deal with this.
There was a problem hiding this comment.
I think this property can be removed now?
| case undefined: | ||
| // allowed root segments | ||
| return type === "login" || type === "session" || type === "sso" || type === "logout"; | ||
| return type === "login" || type === "session" || type === "sso" || type === "logout" || type === "oidc-callback" || type === "oidc-error"; |
There was a problem hiding this comment.
Why not just go with one oidc segment for both an error and callback, with different values? Would reduce the amount of boilerplate I think.
| return t.div({ className: "StartOIDCLoginView" }, | ||
| t.a({ | ||
| className: "StartOIDCLoginView_button button-action secondary", | ||
| href: vm => (vm.isBusy ? "#" : vm.authorizationEndpoint), |
There was a problem hiding this comment.
As mentioned with the view model, I'd only calculate the redirect url once we've clicked this button/link. Once it's clicked, we should show a spinner so people don't think it didn't work. Once the redirect url is known, we programatically redirect.
| this.platform.settingsStorage.setString(`oidc_${p.state}_issuer`, this._api.issuer), | ||
| ]); | ||
|
|
||
| this._authorizationEndpoint = await this._api.authorizationEndpoint(p); |
There was a problem hiding this comment.
Could we postpone this until the button is clicked? It could be confusing that the button doesn't work as long as this request is running. If we do it afterwards, we can show a spinner until we can redirect.
| accessToken: sessionInfo.accessToken, | ||
| accessTokenExpiresAt: sessionInfo.accessTokenExpiresAt, | ||
| refreshToken: sessionInfo.refreshToken, | ||
| anticipation: 30 * 1000, |
There was a problem hiding this comment.
What is this? A margin to make sure we're never too late?
There was a problem hiding this comment.
Yep, we refresh the token 30s before it expires to avoid doing requests with a just-expired token
There was a problem hiding this comment.
Can you add a comment to the token refresher please?
| const { homeserver, issuer } = await lookupHomeserver(initialHomeserver, (url, options) => { | ||
| return setAbortable(this._platform.request(url, options)); | ||
| }); | ||
| if (issuer) { |
There was a problem hiding this comment.
This should follow the same pattern as the other login options (e.g. set all the applicable ones in _parseLoginOptions, and put the code below in a separate class, see SSOLoginHelper), unless there's a reason that wouldn't work?
There was a problem hiding this comment.
The logic behind this is that if the sever advertises OIDC, we most probably want to use it. There will be a time where m.org will support both OIDC-based login and using the legacy endpoints, and we don't want to have a confusing UI during that time?
There was a problem hiding this comment.
Right, I'd prefer that decision to be taken higher up the stack though, in LoginViewModel, and not in SDK-level code. So I'd not prevent other login options from being advertised here in case OIDC support is found, and implement OIDC it just like the other options. And then choose to ignore the other options in the LoginViewModel.
4fe85d2 to
6205478
Compare
9cc6f4b to
eaf876a
Compare
bwindels
left a comment
There was a problem hiding this comment.
Thanks for the changes, I've added some more comments.
| } = options; | ||
| this._request = options.platform.request; | ||
| this._encoding = options.platform.encoding; | ||
| this._crypto = options.platform.crypto; |
There was a problem hiding this comment.
Nit: In view models, there no need to store the properties of the options in member variables as the options are stored in the ViewModel base class. In this case, you can just do this.platform.crypto/encoding/request at any point in a view model.
| this._code = code; | ||
| this._attemptLogin = attemptLogin; | ||
| this._errorMessage = ""; | ||
| this.performOIDCLoginCompletion(); |
There was a problem hiding this comment.
Generally we try (there is some places where we sin though, like LoginViewModel) to not call async methods from the constructor (which can't be async itself), unless there is really no other way and we can be 100% sure the method wont throw.
Usually, we deal with this by adding an async start or init method that is called from the parent view model after creating the child view model.
| oidc: { issuer }, | ||
| }; | ||
| } catch (e) { | ||
| console.log(e); |
There was a problem hiding this comment.
This code might change wrt to the comment above, but wrt to console logging, it is usually only used during development. For production code we use the structured logging api from src/logging/
You could log an operation here by wrapping it in
return this._platform.logger.run("queryLogin", async log => {
// log in here is an ILogItem, happy to explain the API if needed
// pass it through the call stack to log create a tree of log items
});| return window.location.origin; | ||
| } | ||
|
|
||
| createOIDCRedirectURL() { |
There was a problem hiding this comment.
Just realized this is also not in /src/platform/web, but we can clean that up with the method above some other time, so fine for now 👍
| <meta name="apple-mobile-web-app-status-bar-style" content="black"> | ||
| <meta name="apple-mobile-web-app-title" content="Hydrogen Chat"> | ||
| <meta name="description" content="A matrix chat application"> | ||
| <script src="config.js"></script> |
There was a problem hiding this comment.
Can we take this out of this PR though?
| // substr(1) to take of initial / | ||
| const parts = urlPath.substr(1).split("/"); | ||
| const iterator = parts[Symbol.iterator](); | ||
| const segments = []; |
| const iterator = parts[Symbol.iterator](); | ||
| const segments = []; | ||
| let next; | ||
| let next; |
| case "right-panel": | ||
| case "sso": | ||
| case "oidc-callback": | ||
| case "oidc-error": |
There was a problem hiding this comment.
should both of these be just "oidc"?
|
|
||
| get(): C { | ||
| const sourceValue = this.source.get(); | ||
| return this.mapper(sourceValue); |
There was a problem hiding this comment.
if the mapper creates a new object, the result of multiple get calls won't be equal to each other as the mapper is run each time. Probably better to run the mapper on update once and store the result in a member variable.
| uploadProgress?: (loadedBytes: number) => void; | ||
| timeout?: number; | ||
| body?: EncodedBody; | ||
| body?: EncodedBody["body"]; |
de8c561 to
2ad96c7
Compare
7b0e045 to
12d1760
Compare
e23a06b to
f7ffae4
Compare
This also saves the redirectUri during the flow
416ba09 to
d6dff1d
Compare
This is on top of #655, so ignore the first few commits.
There are some changes that could be extracted in separate PRs if needed, including:
ObservableValue#mapmethod, which creates an observable from another one plus a mapper ; basically like#flatMapbut not with theflatpartaccessTokenan Observable in the HSAPI (needed for the refresh token)There are still some stuff to do, especially around teardown, like pausing the token refresher when the session is not active