-
Notifications
You must be signed in to change notification settings - Fork 122
Add support for SSO login #174
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
Conversation
src/api.rs
Outdated
| let (sso_code, sso_code_verifier, callback_url) = | ||
| self.obtain_sso_code(sso_id).await?; | ||
|
|
||
| connect_req = ConnectPasswordReq { |
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'd like to separate it into ConnectPasswordReq and ConnectSSOReq, both "inheriting" fields from ConnectCommonReq, not sure if (or rather how) same effect can be achieved with composition.
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.
something like https://serde.rs/attr-flatten.html would probably be what you want here
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.
Renamed to ConnectTokenReq so it corresponds better to the /connect/token that it's meant for and added subtypes for specific auth methods/strategies.
doy
left a comment
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 overall structure looks good, but i had some feedback on a few of the implementation details
|
|
||
| #[must_use] | ||
| pub fn ui_url(&self) -> String { | ||
| // TODO: default to either vault.bitwarden.com or vault.bitwarden.eu based on the base_url? |
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, i think ideally we would follow the same pattern here that is used in identity_url and notifications_url
src/api.rs
Outdated
| }; | ||
|
|
||
| let callback_server = | ||
| async { start_sso_callback_server(&listener, state.as_str()) }; |
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 isn't going to work like you want - an async block that doesn't await anything isn't actually running asynchronously, it's just going to block the thread when it runs, which isn't what we want. you'll want to use async networking calls (from tokio, or a library which uses it) to make this work.
src/api.rs
Outdated
| ) { | ||
| // TODO: probably it'd be better to display the URL in the console if the automatic | ||
| // open operation fails, instead of failing the whole process? E.g. docker container | ||
| // case |
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, i'd go farther here and say that we should explicitly support both options here (via a command line argument to rbw login or similar)
src/api.rs
Outdated
| .1; | ||
|
|
||
| let states_match = received_state.split("_identifier=").next().unwrap() | ||
| == state.split("_identifier=").next().unwrap(); |
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 are we splitting state here? isn't it just a purely random value?
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.
Hmm, tbh I ported it as-is from the official implementation, it might be that it's used in more contexts there?
As indeed, this split doesn't make sense, state is purely random and is always alphanumeric, so we won't get any collisions with _identitifer=. I'll remove it
| fn start_sso_callback_server( | ||
| listener: &TcpListener, | ||
| state: &str, | ||
| ) -> Result<String> { |
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 general in this function, i'm not particularly comfortable with hand-parsing and writing raw http here. we already pull in enough libraries that we can do this in a less fragile way (i do kind of agree that we don't necessarily need a full blown web server here, but at least using hyper or http or something like that to parse and generate the http would be a lot less fragile)
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've refactored the whole thing to use axum, the worst change that it introduced is using channels for throwing the data around, instead of just the function return values (these now only handle HTTP stuff). A matter of preference I guess
I've tried to go with http and tokio networking calls, but still ended up with hand-crafting HTTP responses (maybe there is some nice http.Response -> bytes crate I wasn't able to find 😅 ) . Also reading from streams in async manner seemed to require more careful handling.
I've also played with hyper directly, but it was giving me strange issues (and still required channels for moving the data around).
With axum we also get query parameters parsing for free.
src/api.rs
Outdated
| let (sso_code, sso_code_verifier, callback_url) = | ||
| self.obtain_sso_code(sso_id).await?; | ||
|
|
||
| connect_req = ConnectPasswordReq { |
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.
something like https://serde.rs/attr-flatten.html would probably be what you want here
"desktop" client_id does not seem to work with refresh token obtained via SSO
Resolves #118
Implementation is mostly ported from the official CLI, with minor changes to utilise having predefined
sso_idin the config file.Tested with Google based SSO.
Auth flow
Can be summarised with:
stateandssoCodeVerifierssoCodeVerifierredirect_urison Bitwarden end)authorization_codevia the callback server/identity/tokenendpoint (same as for passwords, but different params)Quirks
2FA
Due to the way 2FA is implemented in
rbw(try to authenticate without 2FA, fallback to it as needed based on the response from server) SSO users with 2FA configured have to go through the "webbrowser flow" twice: once to get the response about the missing 2FA, second time to send 2FA alongside the SSO obtained code.Bitwarden CLI reuses the authorization_code obtained in first iteration in such cases, but I feel this approach would require bigger overhaul in
rbw.Password
SSO flow doesn't use the master password in its auth, so theoretically we don't have to ask for it during
login.Of course it's still required for the
unlockoperation.I kept the master password prompt in SSO flow to be in sync with the password flow, which automatically calls
unlockafterlogin.Notes
Usual disclaimer: I don't really write Rust, feel free to suggest more idiomatic code wherever you see fit.
Especially the error handling feels like it could use some improvements