-
Notifications
You must be signed in to change notification settings - Fork 44
API client authentication and token granting #1194
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
Depends on dropshot commit c903eb8def228d4597b379b67c4ecc82164a11f8
Needed for "application/x-www-form-urlencoded" body support.
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 is an impressive piece of work! I have a bunch of comments below but most are aimed at clarity and error handling. Thanks for doing this!
-- a token is granted. | ||
-- TODO-security: We should not grant a token more than once per record. | ||
CREATE TABLE omicron.public.client_authentication ( | ||
client_id UUID NOT 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.
It would be great to have a Big Theory Statement-style comment about the client id, device code, user code, etc. It doesn't have to be here -- it could be in the Rust code somewhere. (edit: I suggested putting it in nexus/src/app/client_api.rs)
If there's something like the Beer Drinker's Guide to SAML for the Oauth device flow, that might work too.
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.
Ok, basic flow outline added to app/device_auth.rs
in c9839ba.
common/src/sql/dbinit.sql
Outdated
time_created TIMESTAMPTZ NOT NULL | ||
); | ||
|
||
-- Matches the primary key on client authentication records. |
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: "this is critical for ensuring that no more than one token is ever created for a client authentication attempt"? Like we talked about my fear here is that someone won't realize what's load-bearing here (e.g., that if they change this index in some ways, they might break that at-most-one-for-the-other-table property)
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.
nexus/src/app/client_api.rs
Outdated
@@ -0,0 +1,75 @@ | |||
// This Source Code Form is subject to the terms of the Mozilla Public |
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 we talked about this a bit but I do find the naming here a little confusing. We have lots of clients and lots of tokens, and browsers are clients that authenticate with tokens too. Might this be better called "oauth_device.rs" with oauth_device_authenticate()
etc? I might apply that to the tables too.
I realize this great fodder for bikeshedding and we can always change it later instead but I figured I'd ask.
edit: see also the comment in datastore.rs
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.
Agreed. c9839ba renames client_api
→ device_auth
, which I think is clearer. Function and table names updated as well.
nexus/src/app/client_api.rs
Outdated
use omicron_common::api::external::{CreateResult, LookupResult}; | ||
use uuid::Uuid; | ||
|
||
impl super::Nexus { |
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.
Above this block (or part of the module-level doc comment) might be a good place to give an overview of the oauth device flow and how these functions are used. It may also be nice to have a sentence or two for each function saying which part of the flow it's used in.
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, done in c9839ba.
@@ -111,6 +113,12 @@ pub enum SchemeResult { | |||
Failed(Reason), | |||
} | |||
|
|||
/// A context that can look up a Silo user's Silo. |
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 do you think of renaming this to SiloUserGetSilo
or something? (I know I overuse "context" but this feels like less of a "context" than it was when it was in spoof.rs.)
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 bd8da5f (SiloUserSilo
).
.expect_status(Some(StatusCode::BAD_REQUEST)) | ||
.execute() | ||
.await | ||
.expect("client_id required to start client authentication flow"); |
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'm not sure if you're trying to check the error message here, but this won't do that.
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.
No, it was just poorly worded. Fixed in bbab057.
nexus/src/external_api/client_api.rs
Outdated
}, | ||
}; | ||
|
||
let model = nexus.client_authenticate(&opctx, params.client_id).await?; |
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 this going to do the right thing in terms of the error response form? Say the database is down. It seems like this will generate an Err(Error::ServiceUnavailable)
, which will get turned into a dropshot::HttpError
, which I think we're not supposed to be returning from this function?
If that's true, even if we fix it here, it seems easy to accidentally re-introduce that bug. I wonder if we should have the body of these handlers call a Rust function that returns only Response<Body>
(no Result
). We could document why -- we need to format all the responses ourselves to ensure compliance.
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.
Excellent catch, thank you. I'd like to take this as a follow-up, recorded as #1286.
nexus/src/external_api/client_api.rs
Outdated
&ClientAuthentication::from_model(model, host), | ||
) | ||
}; | ||
// TODO: instrumentation doesn't work because we use `Response<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.
Why doesn't the instrumentation work with that?
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.
Excellent question; I don't actually know! I copied the comment from console_api.rs
, but will investigate as part of #1286.
Currently used only by test_device_auth_flow integration test.
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.
a couple of questions as I try to integrate these changes into the generated client.
#[endpoint { | ||
method = POST, | ||
path = "/device/token", | ||
content_type = "application/x-www-form-urlencoded", |
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'm late to this (sorry) but I thought endpoints using application/x-www-form-urlencoded
were not going to appear in the OpenAPI spec for omicron. Did I misunderstand?
If our client is the consumer of this, why isn't it using application/json
?
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 have strong opinions on whether these should appear in the spec or not; we can set unpublished = true
if you think that's a good idea. Personally I think they should appear, but don't necessarily need full body descriptions if that's not easy. The TS generator, for instance, just skips non-application/json
endpoints, which I think is reasonable, since they'll be used only by OAuth client libraries, not directly by our client.
The reason for not using application/json
is that the OAuth protocol requires application/x-www-form-urlencoded
request bodies, and I would like us not to reinvent that particular wheel. OAuth clients exist for every popular language & framework, and the idea is that we can use off-the-shelf, well-tested client implementations instead of rolling our own n times. That is, we could implement and document our own not-quite-compatible protocol, but I don't see any concrete advantage to doing so. OAuth is not a great protocol, but it's been widely analyzed and implemented, and I think if we're not going to use it, we should have good reasons why we think our own version would be better, more secure, etc. This PR implements only the Device Authorization Grant flow, but we can layer more things on it, such as short-lived access tokens with long-lived refresh tokens. RFD 275 attempts to provide some of this rationale, and is probably the right place for a serious discussion of this issue.
Finally, I suppose it's fair to ask why we should write our own OAuth server but not the client libraries. To this I would respond that (1) we only need one server implementation, whereas for the clients we'd need n clients with specialized token, error, retry, and timeout handling, and (2) to the best of my understanding, we want there to be a single source of truth server-side (CockroachDB) and a single piece of software (Nexus) that handles all client requests and talks to that database, provides authz for it, etc. I did look into off-the-shelf OAuth servers, but none of them seemed suitable for our needs. OTOH, the client libraries are readily available and easy to integrate.
method = POST, | ||
path = "/device/auth", | ||
content_type = "application/x-www-form-urlencoded", | ||
tags = ["hidden"], // "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.
This seems important for OAuth, but can we set unpublished = true
since I don't think we expect other consumers to use this?
Support OAuth 2.0 Device Authorization Grant for client (e.g., CLI) authentication. Remaining TODOs:
application/x-www-form-urlencoded
bodies)Dependents: