-
Notifications
You must be signed in to change notification settings - Fork 128
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
Draft: OIDC native login #660
base: master
Are you sure you want to change the base?
Conversation
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider only passing in the oidc login method here, as you don't need the other ones?
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 did it this way so it is consistent with StartSSOLoginViewModel
src/matrix/net/OidcApi.ts
Outdated
): 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can use platform.crypto.digest("SHA-256", data)
here, which deals better with cross-browser differences and maybe one day different platforms.
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.
Done in ea1bcd1
src/matrix/net/OidcApi.ts
Outdated
async _generateCodeChallenge( | ||
codeVerifier: string | ||
): Promise<string> { | ||
const encoder = new TextEncoder(); |
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.
You can use platform.encoding.utf8.encode()
here, which deals better with cross-browser differences and maybe one day different platforms.
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.
Done in ea1bcd1
src/matrix/net/OidcApi.ts
Outdated
} | ||
|
||
get redirectUri() { | ||
return window.location.origin; |
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 69a3404
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 think this property can be removed now?
src/domain/navigation/index.js
Outdated
switch (parent?.type) { | ||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
Done!
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
Done in 6205478
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
Done in 6205478
accessToken: sessionInfo.accessToken, | ||
accessTokenExpiresAt: sessionInfo.accessTokenExpiresAt, | ||
refreshToken: sessionInfo.refreshToken, | ||
anticipation: 30 * 1000, |
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.
What is this? A margin to make sure we're never too late?
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment to the token refresher please?
return setAbortable(this._platform.request(url, options)); | ||
}); | ||
if (issuer) { |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
});
src/domain/navigation/URLRouter.js
Outdated
@@ -125,6 +125,10 @@ export class URLRouter { | |||
return window.location.origin; | |||
} | |||
|
|||
createOIDCRedirectURL() { |
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.
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 👍
src/platform/web/index.html
Outdated
@@ -9,6 +9,7 @@ | |||
<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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we take this out of this PR though?
src/domain/navigation/index.js
Outdated
// substr(1) to take of initial / | ||
const parts = urlPath.substr(1).split("/"); | ||
const iterator = parts[Symbol.iterator](); | ||
const segments = []; |
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 is this removed?
src/domain/navigation/index.js
Outdated
// substr(1) to take of initial / | ||
const parts = urlPath.substr(1).split("/"); | ||
const iterator = parts[Symbol.iterator](); | ||
const segments = []; | ||
let next; | ||
let next; |
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.
What is going on here?
src/domain/navigation/index.js
Outdated
@@ -191,6 +215,8 @@ export function stringifyPath(path) { | |||
break; | |||
case "right-panel": | |||
case "sso": | |||
case "oidc-callback": | |||
case "oidc-error": |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
src/platform/types/types.ts
Outdated
@@ -21,11 +21,12 @@ import type {ILogItem} from "../../logging/types"; | |||
export interface IRequestOptions { | |||
uploadProgress?: (loadedBytes: number) => void; | |||
timeout?: number; | |||
body?: EncodedBody; | |||
body?: EncodedBody["body"]; |
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.
what is this about?
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#map
method, which creates an observable from another one plus a mapper ; basically like#flatMap
but not with theflat
partaccessToken
an 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