-
Notifications
You must be signed in to change notification settings - Fork 100
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
Add support for SSO login #174
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.
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
@@ -174,6 +178,14 @@ impl Config { | |||
}) | |||
} | |||
|
|||
#[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_id
in the config file.Tested with Google based SSO.
Auth flow
Can be summarised with:
state
andssoCodeVerifier
ssoCodeVerifier
redirect_uris
on Bitwarden end)authorization_code
via the callback server/identity/token
endpoint (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
unlock
operation.I kept the master password prompt in SSO flow to be in sync with the password flow, which automatically calls
unlock
afterlogin
.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