From 787778b2f9d656a1c60734bbcc3a7c189b15211c Mon Sep 17 00:00:00 2001 From: Alex Plotnick Date: Thu, 7 Jul 2022 13:17:33 -0600 Subject: [PATCH] Refactor device auth schema (#1344) Device auth request records are now short-lived so that we can use `user_code` as a primary key. Datastore authz is implemented for all but access token lookup, which needs to be looked up both by `token` (primary key) and the unique combination of `client_id` and `device_code`. --- common/src/api/external/mod.rs | 2 + common/src/sql/dbinit.sql | 29 +++-- nexus/src/app/device_auth.rs | 114 ++++++++++++++++---- nexus/src/authz/api_resources.rs | 72 +++++++++++++ nexus/src/authz/omicron.polar | 15 +++ nexus/src/authz/oso_generic.rs | 3 + nexus/src/db/datastore.rs | 148 ++++++++++++++------------ nexus/src/db/lookup.rs | 56 ++++++++++ nexus/src/db/model/device_auth.rs | 22 +++- nexus/src/db/schema.rs | 6 +- nexus/src/external_api/device_auth.rs | 11 +- 11 files changed, 364 insertions(+), 114 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index c0e92841ed..83bb373b2a 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -518,6 +518,8 @@ pub enum ResourceType { SamlIdentityProvider, SshKey, ConsoleSession, + DeviceAuthRequest, + DeviceAccessToken, GlobalImage, Organization, Project, diff --git a/common/src/sql/dbinit.sql b/common/src/sql/dbinit.sql index 83958afa9f..c500848123 100644 --- a/common/src/sql/dbinit.sql +++ b/common/src/sql/dbinit.sql @@ -1252,23 +1252,19 @@ INSERT INTO omicron.public.user_builtin ( * OAuth 2.0 Device Authorization Grant (RFC 8628) */ --- Device authorization requests. In theory these records could --- (and probably should) be short-lived, and removed as soon as --- a token is granted. --- TODO-security: We should not grant a token more than once per record. +-- Device authorization requests. These records are short-lived, +-- and removed as soon as a token is granted. This allows us to +-- use the `user_code` as primary key, despite it not having very +-- much entropy. +-- TODO: A background task should remove unused expired records. CREATE TABLE omicron.public.device_auth_request ( + user_code STRING(20) PRIMARY KEY, client_id UUID NOT NULL, device_code STRING(40) NOT NULL, - user_code STRING(63) NOT NULL, time_created TIMESTAMPTZ NOT NULL, - time_expires TIMESTAMPTZ NOT NULL, - - PRIMARY KEY (client_id, device_code) + time_expires TIMESTAMPTZ NOT NULL ); --- Fast lookup by user_code for verification -CREATE INDEX ON omicron.public.device_auth_request (user_code); - -- Access tokens granted in response to successful device authorization flows. -- TODO-security: expire tokens. CREATE TABLE omicron.public.device_access_token ( @@ -1276,13 +1272,16 @@ CREATE TABLE omicron.public.device_access_token ( client_id UUID NOT NULL, device_code STRING(40) NOT NULL, silo_user_id UUID NOT NULL, - time_created TIMESTAMPTZ NOT NULL + time_requested TIMESTAMPTZ NOT NULL, + time_created TIMESTAMPTZ NOT NULL, + time_expires TIMESTAMPTZ ); --- Matches the primary key on device authorization records. --- The UNIQUE constraint is critical for ensuring that at most +-- This UNIQUE constraint is critical for ensuring that at most -- one token is ever created for a given device authorization flow. -CREATE UNIQUE INDEX ON omicron.public.device_access_token (client_id, device_code); +CREATE UNIQUE INDEX ON omicron.public.device_access_token ( + client_id, device_code +); /* * Roles built into the system diff --git a/nexus/src/app/device_auth.rs b/nexus/src/app/device_auth.rs index 932a247b72..368d866c7d 100644 --- a/nexus/src/app/device_auth.rs +++ b/nexus/src/app/device_auth.rs @@ -29,8 +29,8 @@ //! automatic case, it may supply the `user_code` as a query parameter. //! 5. The user logs in using their configured IdP, then enters or verifies //! the `user_code`. -//! 6. On successful login, the server is notified and responds to the -//! poll started in step 3 with a freshly granted access token. +//! 6. On successful login, the server responds to the poll started +//! in step 3 with a freshly granted access token. //! //! Note that in this flow, there are actually two distinct sets of //! connections made to the server: by the client itself, and by the @@ -45,44 +45,94 @@ //! but that may change in the future. use crate::authn::{Actor, Reason}; +use crate::authz; use crate::context::OpContext; +use crate::db::lookup::LookupPath; use crate::db::model::{DeviceAccessToken, DeviceAuthRequest}; use crate::external_api::device_auth::DeviceAccessTokenResponse; -use omicron_common::api::external::CreateResult; + +use omicron_common::api::external::{CreateResult, Error}; + +use chrono::Utc; use uuid::Uuid; impl super::Nexus { /// Start a device authorization grant flow. /// Corresponds to steps 1 & 2 in the flow description above. - pub async fn device_auth_request( + pub async fn device_auth_request_create( &self, opctx: &OpContext, client_id: Uuid, ) -> CreateResult { + // TODO-correctness: the `user_code` generated for a new request + // is used as a primary key, but may potentially collide with an + // existing outstanding request. So we should retry some (small) + // number of times if inserting the new request fails. let auth_request = DeviceAuthRequest::new(client_id); - self.db_datastore.device_auth_start(opctx, auth_request).await + self.db_datastore.device_auth_request_create(opctx, auth_request).await } - /// Verify a device authorization grant. + /// Verify a device authorization grant, and delete the authorization + /// request so that at most one token will be granted per request. + /// Invoked in response to a request from the browser, not the client. /// Corresponds to step 5 in the flow description above. - pub async fn device_auth_verify( + pub async fn device_auth_request_verify( &self, opctx: &OpContext, user_code: String, silo_user_id: Uuid, ) -> CreateResult { - let auth_request = - self.db_datastore.device_auth_get_request(opctx, user_code).await?; - let client_id = auth_request.client_id; - let device_code = auth_request.device_code; - let token = - DeviceAccessToken::new(client_id, device_code, silo_user_id); - self.db_datastore.device_auth_grant(opctx, token).await + let (.., authz_request, db_request) = + LookupPath::new(opctx, &self.db_datastore) + .device_auth_request(&user_code) + .fetch() + .await?; + + let (.., authz_user) = LookupPath::new(opctx, &self.datastore()) + .silo_user_id(silo_user_id) + .lookup_for(authz::Action::CreateChild) + .await?; + assert_eq!(authz_user.id(), silo_user_id); + + // Create an access token record. + let token = DeviceAccessToken::new( + db_request.client_id, + db_request.device_code, + db_request.time_created, + silo_user_id, + ); + + if db_request.time_expires < Utc::now() { + // Store the expired token anyway so that the client + // can get a proper "denied" message on its next poll. + let token = token.expires(db_request.time_expires); + self.db_datastore + .device_access_token_create( + opctx, + &authz_request, + &authz_user, + token, + ) + .await?; + Err(Error::InvalidRequest { + message: "device authorization request expired".to_string(), + }) + } else { + // TODO-security: set an expiration time for the valid token. + self.db_datastore + .device_access_token_create( + opctx, + &authz_request, + &authz_user, + token, + ) + .await + } } /// Look up a possibly-not-yet-granted device access token. /// Corresponds to steps 3 & 6 in the flow description above. - pub async fn device_access_token_lookup( + pub async fn device_access_token_fetch( &self, opctx: &OpContext, client_id: Uuid, @@ -91,7 +141,7 @@ impl super::Nexus { use DeviceAccessTokenResponse::*; match self .db_datastore - .device_auth_get_token(opctx, client_id, device_code) + .device_access_token_fetch(opctx, client_id, device_code) .await { Ok(token) => Ok(Granted(token)), @@ -107,12 +157,30 @@ impl super::Nexus { opctx: &OpContext, token: String, ) -> Result { - match self.db_datastore.device_access_token_actor(opctx, token).await { - Ok(actor) => Ok(actor), - // TODO: better error handling - Err(_) => Err(Reason::UnknownActor { - actor: String::from("from bearer token"), - }), - } + let (.., db_access_token) = LookupPath::new(opctx, &self.db_datastore) + .device_access_token(&token) + .fetch() + .await + .map_err(|e| match e { + Error::ObjectNotFound { .. } => Reason::UnknownActor { + actor: "from device access token".to_string(), + }, + e => Reason::UnknownError { source: e }, + })?; + + let silo_user_id = db_access_token.silo_user_id; + let (.., db_silo_user) = LookupPath::new(opctx, &self.db_datastore) + .silo_user_id(silo_user_id) + .fetch() + .await + .map_err(|e| match e { + Error::ObjectNotFound { .. } => { + Reason::UnknownActor { actor: silo_user_id.to_string() } + } + e => Reason::UnknownError { source: e }, + })?; + let silo_id = db_silo_user.silo_id; + + Ok(Actor::SiloUser { silo_user_id, silo_id }) } } diff --git a/nexus/src/authz/api_resources.rs b/nexus/src/authz/api_resources.rs index a17568a5b7..b684a608a5 100644 --- a/nexus/src/authz/api_resources.rs +++ b/nexus/src/authz/api_resources.rs @@ -239,6 +239,8 @@ impl db::model::DatabaseString for FleetRole { } } +// TODO: refactor synthetic resources below + /// ConsoleSessionList is a synthetic resource used for modeling who has access /// to create sessions. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -421,6 +423,60 @@ impl AuthorizedResource for IpPoolList { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DeviceAuthRequestList; +/// Singleton representing the [`DeviceAuthRequestList`] itself for authz purposes +pub const DEVICE_AUTH_REQUEST_LIST: DeviceAuthRequestList = + DeviceAuthRequestList; + +impl oso::PolarClass for DeviceAuthRequestList { + fn get_polar_class_builder() -> oso::ClassBuilder { + oso::Class::builder() + .with_equality_check() + .add_attribute_getter("fleet", |_| FLEET) + } +} + +impl AuthorizedResource for DeviceAuthRequestList { + fn load_roles<'a, 'b, 'c, 'd, 'e, 'f>( + &'a self, + opctx: &'b OpContext, + datastore: &'c DataStore, + authn: &'d authn::Context, + roleset: &'e mut RoleSet, + ) -> futures::future::BoxFuture<'f, Result<(), Error>> + where + 'a: 'f, + 'b: 'f, + 'c: 'f, + 'd: 'f, + 'e: 'f, + { + // There are no roles on the DeviceAuthRequestList, only permissions. But we + // still need to load the Fleet-related roles to verify that the actor has the + // "admin" role on the Fleet. + load_roles_for_resource( + opctx, + datastore, + authn, + ResourceType::Fleet, + *FLEET_ID, + roleset, + ) + .boxed() + } + + fn on_unauthorized( + &self, + _: &Authz, + error: Error, + _: AnyActor, + _: Action, + ) -> Error { + error + } +} + // Main resource hierarchy: Organizations, Projects, and their resources authz_resource! { @@ -602,6 +658,22 @@ authz_resource! { polar_snippet = FleetChild, } +authz_resource! { + name = "DeviceAuthRequest", + parent = "Fleet", + primary_key = String, // user_code + roles_allowed = false, + polar_snippet = FleetChild, +} + +authz_resource! { + name = "DeviceAccessToken", + parent = "Fleet", + primary_key = String, // token + roles_allowed = false, + polar_snippet = FleetChild, +} + authz_resource! { name = "RoleBuiltin", parent = "Fleet", diff --git a/nexus/src/authz/omicron.polar b/nexus/src/authz/omicron.polar index 2e39545a5c..59e0952b96 100644 --- a/nexus/src/authz/omicron.polar +++ b/nexus/src/authz/omicron.polar @@ -355,6 +355,15 @@ resource ConsoleSessionList { has_relation(fleet: Fleet, "parent_fleet", collection: ConsoleSessionList) if collection.fleet = fleet; +# Describes the policy for creating and managing device authorization requests. +resource DeviceAuthRequestList { + permissions = [ "create_child" ]; + relations = { parent_fleet: Fleet }; + "create_child" if "external-authenticator" on "parent_fleet"; +} +has_relation(fleet: Fleet, "parent_fleet", collection: DeviceAuthRequestList) + if collection.fleet = fleet; + # These rules grants the external authenticator role the permissions it needs to # read silo users and modify their sessions. This is necessary for login to # work. @@ -362,11 +371,17 @@ has_permission(actor: AuthenticatedActor, "read", silo: Silo) if has_role(actor, "external-authenticator", silo.fleet); has_permission(actor: AuthenticatedActor, "read", user: SiloUser) if has_role(actor, "external-authenticator", user.silo.fleet); + has_permission(actor: AuthenticatedActor, "read", session: ConsoleSession) if has_role(actor, "external-authenticator", session.fleet); has_permission(actor: AuthenticatedActor, "modify", session: ConsoleSession) if has_role(actor, "external-authenticator", session.fleet); +has_permission(actor: AuthenticatedActor, "read", device_auth: DeviceAuthRequest) + if has_role(actor, "external-authenticator", device_auth.fleet); +has_permission(actor: AuthenticatedActor, "read", device_token: DeviceAccessToken) + if has_role(actor, "external-authenticator", device_token.fleet); + has_permission(actor: AuthenticatedActor, "read", identity_provider: IdentityProvider) if has_role(actor, "external-authenticator", identity_provider.silo.fleet); has_permission(actor: AuthenticatedActor, "list_identity_providers", identity_provider: IdentityProvider) diff --git a/nexus/src/authz/oso_generic.rs b/nexus/src/authz/oso_generic.rs index 9f5cda930f..80cad8b4a1 100644 --- a/nexus/src/authz/oso_generic.rs +++ b/nexus/src/authz/oso_generic.rs @@ -46,6 +46,7 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result { IpPoolList::get_polar_class(), GlobalImageList::get_polar_class(), ConsoleSessionList::get_polar_class(), + DeviceAuthRequestList::get_polar_class(), ]; for c in classes { info!(log, "registering Oso class"; "class" => &c.name); @@ -66,6 +67,8 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result { VpcSubnet::init(), // Fleet-level resources ConsoleSession::init(), + DeviceAuthRequest::init(), + DeviceAccessToken::init(), Rack::init(), RoleBuiltin::init(), SshKey::init(), diff --git a/nexus/src/db/datastore.rs b/nexus/src/db/datastore.rs index 44f0a80052..6c3b7c0580 100644 --- a/nexus/src/db/datastore.rs +++ b/nexus/src/db/datastore.rs @@ -25,7 +25,7 @@ use super::error::diesel_pool_result_optional; use super::identity::{Asset, Resource}; use super::pool::DbConnection; use super::Pool; -use crate::authn::{self, Actor}; +use crate::authn; use crate::authz::{self, ApiResource}; use crate::context::OpContext; use crate::db::collection_attach::{AttachError, DatastoreAttachTarget}; @@ -4299,12 +4299,18 @@ impl DataStore { /// Start a device authorization grant flow by recording the request /// and initial response parameters. - // TODO-security: authz - pub async fn device_auth_start( + pub async fn device_auth_request_create( &self, opctx: &OpContext, auth_request: DeviceAuthRequest, ) -> CreateResult { + opctx + .authorize( + authz::Action::CreateChild, + &authz::DEVICE_AUTH_REQUEST_LIST, + ) + .await?; + use db::schema::device_auth_request::dsl; diesel::insert_into(dsl::device_auth_request) .values(auth_request) @@ -4314,45 +4320,74 @@ impl DataStore { .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) } - /// Look up a device authorization request by `user_code`. - // TODO-security: authz - pub async fn device_auth_get_request( - &self, - opctx: &OpContext, - user_code: String, - ) -> LookupResult { - use db::schema::device_auth_request::dsl; - dsl::device_auth_request - .filter(dsl::user_code.eq(user_code)) - .filter(dsl::time_expires.gt(Utc::now())) - .select(DeviceAuthRequest::as_select()) - .get_result_async(self.pool_authorized(opctx).await?) - .await - .map_err(|e| { - // TODO-correctness: better error (not found) - public_error_from_diesel_pool(e, ErrorHandler::Server) - }) - } - - /// Grant a device authorization token. - // TODO-security: authz - pub async fn device_auth_grant( + /// Remove the device authorization request and create a new device + /// access token record. The token may already be expired if the flow + /// was not completed in time. + pub async fn device_access_token_create( &self, opctx: &OpContext, + authz_request: &authz::DeviceAuthRequest, + authz_user: &authz::SiloUser, access_token: DeviceAccessToken, ) -> CreateResult { - use db::schema::device_access_token::dsl; - diesel::insert_into(dsl::device_access_token) + assert_eq!(authz_user.id(), access_token.silo_user_id); + opctx.authorize(authz::Action::Delete, authz_request).await?; + opctx.authorize(authz::Action::CreateChild, authz_user).await?; + + use db::schema::device_auth_request::dsl as request_dsl; + let delete_request = diesel::delete(request_dsl::device_auth_request) + .filter(request_dsl::user_code.eq(authz_request.id())); + + use db::schema::device_access_token::dsl as token_dsl; + let insert_token = diesel::insert_into(token_dsl::device_access_token) .values(access_token) - .returning(DeviceAccessToken::as_returning()) - .get_result_async(self.pool_authorized(opctx).await?) + .returning(DeviceAccessToken::as_returning()); + + #[derive(Debug)] + enum TokenGrantError { + RequestNotFound, + TooManyRequests, + } + type TxnError = TransactionError; + + self.pool_authorized(opctx) + .await? + .transaction(move |conn| match delete_request.execute(conn)? { + 0 => { + Err(TxnError::CustomError(TokenGrantError::RequestNotFound)) + } + 1 => Ok(insert_token.get_result(conn)?), + _ => Err(TxnError::CustomError( + TokenGrantError::TooManyRequests, + )), + }) .await - .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) + .map_err(|e| match e { + TxnError::CustomError(TokenGrantError::RequestNotFound) => { + Error::ObjectNotFound { + type_name: ResourceType::DeviceAuthRequest, + lookup_type: LookupType::ByCompositeId( + authz_request.id(), + ), + } + } + TxnError::CustomError(TokenGrantError::TooManyRequests) => { + Error::internal_error("unexpectedly found multiple device auth requests for the same user code") + } + TxnError::Pool(e) => { + public_error_from_diesel_pool(e, ErrorHandler::Server) + } + }) } - /// Look up a granted device authorization token. - // TODO-security: authz - pub async fn device_auth_get_token( + /// Look up a granted device access token. + /// Note: since this lookup is not by a primary key or name, + /// (though it does use a unique index), it does not fit the + /// usual lookup machinery pattern. It therefore does include + /// any authz checks. However, the device code is a single-use + /// high-entropy random token, and so should not be guessable + /// by an attacker. + pub async fn device_access_token_fetch( &self, opctx: &OpContext, client_id: Uuid, @@ -4366,45 +4401,18 @@ impl DataStore { .get_result_async(self.pool_authorized(opctx).await?) .await .map_err(|e| { - // TODO-correctness: better error (not found) - public_error_from_diesel_pool(e, ErrorHandler::Server) + public_error_from_diesel_pool( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::DeviceAccessToken, + LookupType::ByCompositeId( + "client_id, device_code".to_string(), + ), + ), + ) }) } - /// Look up the actor (a Silo user) for whom a token was granted. - // TODO-security: authz - pub async fn device_access_token_actor( - &self, - opctx: &OpContext, - token: String, - ) -> LookupResult { - use db::schema::device_access_token::dsl; - use db::schema::silo_user::dsl as silo_user_dsl; - - let pool = self.pool_authorized(opctx).await?; - let token = dsl::device_access_token - .filter(dsl::token.eq(token)) - .select(DeviceAccessToken::as_select()) - .get_result_async(pool) - .await - .map_err(|e| { - // TODO-correctness: better error (not found) - public_error_from_diesel_pool(e, ErrorHandler::Server) - })?; - let silo_user_id = token.silo_user_id; - let silo_id = silo_user_dsl::silo_user - .filter(silo_user_dsl::id.eq(silo_user_id)) - .select(SiloUser::as_select()) - .get_result_async(pool) - .await - .map_err(|e| { - // TODO-correctness: better error (not found) - public_error_from_diesel_pool(e, ErrorHandler::Server) - })? - .silo_id; - Ok(Actor::SiloUser { silo_user_id, silo_id }) - } - // Test interfaces #[cfg(test)] diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 7b6df46900..45306acba0 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -290,6 +290,40 @@ impl<'a> LookupPath<'a> { } } + /// Select a resource of type DeviceAuthRequest, identified by its `user_code` + pub fn device_auth_request<'b, 'c>( + self, + user_code: &'b str, + ) -> DeviceAuthRequest<'c> + where + 'a: 'c, + 'b: 'c, + { + DeviceAuthRequest { + key: DeviceAuthRequestKey::PrimaryKey( + Root { lookup_root: self }, + user_code.to_string(), + ), + } + } + + /// Select a resource of type DeviceAccessToken, identified by its `token` + pub fn device_access_token<'b, 'c>( + self, + token: &'b str, + ) -> DeviceAccessToken<'c> + where + 'a: 'c, + 'b: 'c, + { + DeviceAccessToken { + key: DeviceAccessTokenKey::PrimaryKey( + Root { lookup_root: self }, + token.to_string(), + ), + } + } + /// Select a resource of type RoleBuiltin, identified by its `name` pub fn role_builtin_name(self, name: &str) -> RoleBuiltin<'a> { let parts = name.split_once('.'); @@ -566,6 +600,28 @@ lookup_resource! { ] } +lookup_resource! { + name = "DeviceAuthRequest", + ancestors = [], + children = [], + lookup_by_name = false, + soft_deletes = false, + primary_key_columns = [ + { column_name = "user_code", rust_type = String }, + ] +} + +lookup_resource! { + name = "DeviceAccessToken", + ancestors = [], + children = [], + lookup_by_name = false, + soft_deletes = false, + primary_key_columns = [ + { column_name = "token", rust_type = String }, + ] +} + lookup_resource! { name = "RoleBuiltin", ancestors = [], diff --git a/nexus/src/db/model/device_auth.rs b/nexus/src/db/model/device_auth.rs index ca9fd0a699..b0a0703c5e 100644 --- a/nexus/src/db/model/device_auth.rs +++ b/nexus/src/db/model/device_auth.rs @@ -85,6 +85,10 @@ impl DeviceAuthRequest { + Duration::seconds(CLIENT_AUTHENTICATION_TIMEOUT), } } + + pub fn id(&self) -> String { + self.user_code.clone() + } } /// An access token granted in response to a successful device authorization flow. @@ -96,23 +100,39 @@ pub struct DeviceAccessToken { pub client_id: Uuid, pub device_code: String, pub silo_user_id: Uuid, + pub time_requested: DateTime, pub time_created: DateTime, + pub time_expires: Option>, } impl DeviceAccessToken { pub fn new( client_id: Uuid, device_code: String, + time_requested: DateTime, silo_user_id: Uuid, ) -> Self { + let now = Utc::now(); + assert!(time_requested <= now); Self { token: generate_token(), client_id, device_code, silo_user_id, - time_created: Utc::now(), + time_requested, + time_created: now, + time_expires: None, } } + + pub fn id(&self) -> String { + self.token.clone() + } + + pub fn expires(mut self, time: DateTime) -> Self { + self.time_expires = Some(time); + self + } } #[cfg(test)] diff --git a/nexus/src/db/schema.rs b/nexus/src/db/schema.rs index 06a5504aed..8a8cd00b90 100644 --- a/nexus/src/db/schema.rs +++ b/nexus/src/db/schema.rs @@ -514,10 +514,10 @@ table! { } table! { - device_auth_request (client_id, device_code) { + device_auth_request (user_code) { + user_code -> Text, client_id -> Uuid, device_code -> Text, - user_code -> Text, time_created -> Timestamptz, time_expires -> Timestamptz, } @@ -529,7 +529,9 @@ table! { client_id -> Uuid, device_code -> Text, silo_user_id -> Uuid, + time_requested -> Timestamptz, time_created -> Timestamptz, + time_expires -> Nullable, } } diff --git a/nexus/src/external_api/device_auth.rs b/nexus/src/external_api/device_auth.rs index 746b973716..21d55eae4f 100644 --- a/nexus/src/external_api/device_auth.rs +++ b/nexus/src/external_api/device_auth.rs @@ -95,7 +95,8 @@ pub async fn device_auth_request( }, }; - let model = nexus.device_auth_request(&opctx, params.client_id).await?; + let model = + nexus.device_auth_request_create(&opctx, params.client_id).await?; build_oauth_response( StatusCode::OK, &DeviceAuthResponse::from_model(model, host), @@ -161,7 +162,11 @@ pub async fn device_auth_confirm( let opctx = OpContext::for_external_api(&rqctx).await?; let &actor = opctx.authn.actor_required()?; let _token = nexus - .device_auth_verify(&opctx, params.user_code, actor.actor_id()) + .device_auth_request_verify( + &opctx, + params.user_code, + actor.actor_id(), + ) .await?; Ok(HttpResponseOk(())) }; @@ -214,7 +219,7 @@ pub async fn device_access_token( let opctx = nexus.opctx_external_authn(); use DeviceAccessTokenResponse::*; match nexus - .device_access_token_lookup( + .device_access_token_fetch( &opctx, params.client_id, params.device_code,