Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nexus/db-model/src/device_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ impl From<DeviceAccessToken> for views::DeviceAccessTokenGrant {
Self {
access_token: format!("oxide-token-{}", access_token.token),
token_type: views::DeviceAccessTokenType::Bearer,
token_id: access_token.id.into_untyped_uuid(),
time_expires: access_token.time_expires,
}
}
}
Expand Down
29 changes: 21 additions & 8 deletions nexus/tests/integration_tests/device_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ async fn test_device_auth_flow(cptestctx: &ControlPlaneTestContext) {
assert_eq!(get_tokens_priv(testctx).await.len(), 0);
let tokens_unpriv_after = get_tokens_unpriv(testctx).await;
assert_eq!(tokens_unpriv_after.len(), 1);
assert_eq!(tokens_unpriv_after[0].id, token.token_id);
assert_eq!(token.time_expires, None);
assert_eq!(tokens_unpriv_after[0].time_expires, None);

// now make a request with the token. it 403s because unpriv user has no
Expand Down Expand Up @@ -241,7 +243,9 @@ async fn test_device_auth_flow(cptestctx: &ControlPlaneTestContext) {

/// Helper to make the test cute. Goes through the whole flow, returns the token
/// as a string
async fn get_device_token(testctx: &ClientTestContext) -> String {
async fn get_device_token(
testctx: &ClientTestContext,
) -> DeviceAccessTokenGrant {
let client_id = Uuid::new_v4();
let authn_params = DeviceAuthRequest { client_id, ttl_seconds: None };

Expand Down Expand Up @@ -279,8 +283,8 @@ async fn get_device_token(testctx: &ClientTestContext) -> String {
client_id,
};

// Get the token
let token: DeviceAccessTokenGrant = NexusRequest::new(
// Get the token and return it
NexusRequest::new(
RequestBuilder::new(testctx, Method::POST, "/device/token")
.allow_non_dropshot_errors()
.body_urlencoded(Some(&token_params))
Expand All @@ -291,9 +295,7 @@ async fn get_device_token(testctx: &ClientTestContext) -> String {
.await
.expect("failed to get token")
.parsed_body()
.expect("failed to deserialize token response");

token.access_token
.expect("failed to deserialize token response")
}

#[nexus_test]
Expand All @@ -309,12 +311,14 @@ async fn test_device_token_expiration(cptestctx: &ControlPlaneTestContext) {

// get a token for the privileged user. default silo max token expiration
// is null, so tokens don't expire
let initial_token = get_device_token(testctx).await;
let initial_token_grant = get_device_token(testctx).await;
let initial_token = initial_token_grant.access_token;

// now there is a token in the list
let tokens = get_tokens_priv(testctx).await;
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].time_expires, None);
assert_eq!(tokens[0].id, initial_token_grant.token_id);

// test token works on project list
project_list(&testctx, &initial_token, StatusCode::OK)
Expand Down Expand Up @@ -377,7 +381,16 @@ async fn test_device_token_expiration(cptestctx: &ControlPlaneTestContext) {
assert_eq!(settings.device_token_max_ttl_seconds, Some(3));

// create token again (this one will have the 3-second expiration)
let expiring_token = get_device_token(testctx).await;
let expiring_token_grant = get_device_token(testctx).await;

// check that expiration time is there and in the right range
let exp = expiring_token_grant
.time_expires
.expect("Expiring token should have an expiration time");
let exp = (exp - Utc::now()).num_seconds();
assert!(exp > 0 && exp < 5, "should be around 3 seconds from now");

let expiring_token = expiring_token_grant.access_token;

// use a block so we don't touch expiring_token
{
Expand Down
16 changes: 12 additions & 4 deletions nexus/types/src/external_api/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,8 @@ pub struct DeviceAccessToken {
/// "oxide-token-"
pub id: Uuid,
pub time_created: DateTime<Utc>,

/// Expiration timestamp. A null value means the token does not automatically expire.
pub time_expires: Option<DateTime<Utc>>,
}

Expand All @@ -1012,29 +1014,35 @@ impl SimpleIdentity for DeviceAccessToken {
/// See RFC 8628 §3.2 (Device Authorization Response).
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct DeviceAuthResponse {
/// The device verification code.
/// The device verification code
pub device_code: String,

/// The end-user verification code.
/// The end-user verification code
pub user_code: String,

/// The end-user verification URI on the authorization server.
/// The URI should be short and easy to remember as end users
/// may be asked to manually type it into their user agent.
pub verification_uri: String,

/// The lifetime in seconds of the `device_code` and `user_code`.
/// The lifetime in seconds of the `device_code` and `user_code`
pub expires_in: u16,
}

/// Successful access token grant. See RFC 6749 §5.1.
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct DeviceAccessTokenGrant {
/// The access token issued to the client.
/// The access token issued to the client
pub access_token: String,

/// The type of the token issued, as described in RFC 6749 §7.1.
pub token_type: DeviceAccessTokenType,

/// A unique, immutable, system-controlled identifier for the token
pub token_id: Uuid,

/// Expiration timestamp. A null value means the token does not automatically expire.
pub time_expires: Option<DateTime<Utc>>,
}

/// The kind of token granted.
Expand Down
1 change: 1 addition & 0 deletions openapi/nexus.json
Original file line number Diff line number Diff line change
Expand Up @@ -16620,6 +16620,7 @@
},
"time_expires": {
"nullable": true,
"description": "Expiration timestamp. A null value means the token does not automatically expire.",
"type": "string",
"format": "date-time"
}
Expand Down
Loading