Skip to content

Commit fd75f3c

Browse files
feat: add VPC authentication routes and signature verification (#218)
* feat: add VPC authentication routes and signature verification - Introduced new VPC authentication routes, including a login endpoint that verifies client signatures and generates access tokens. - Implemented HMAC-SHA256 signature verification for VPC requests, enhancing security. - Added comprehensive end-to-end tests for VPC login functionality, covering success cases and various failure scenarios. - Updated dependencies to include `hmac` and `sha2` for cryptographic operations. - Refactored the AttestationService to support VPC signature verification and manage shared secrets. * refactor: update VPC login tests for improved validation - Changed API key validation to check for non-empty values instead of format. - Updated user response assertions to ensure both user ID and email are non-empty. - Enhanced comments for clarity regarding mock authentication behavior. * feat: get vpc shared secret from file * test: drop temp files --------- Co-authored-by: Robert Yan <46699230+think-in-universe@users.noreply.github.com>
1 parent 2df9ab1 commit fd75f3c

9 files changed

Lines changed: 666 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/api/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,4 @@ base64 = "0.22.1"
5353
dotenvy = "0.15.7"
5454
k256 = { version = "0.13", features = ["ecdsa", "arithmetic"] }
5555
sha3 = "0.10"
56+
hmac = "0.12"

crates/api/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,8 @@ pub fn build_app_with_config(
485485
let invitation_routes =
486486
build_invitation_routes(app_state.clone(), &auth_components.auth_state_middleware);
487487

488+
let auth_vpc_routes = build_auth_vpc_routes(app_state.clone());
489+
488490
let files_routes =
489491
build_files_routes(app_state.clone(), &auth_components.auth_state_middleware);
490492

@@ -508,12 +510,23 @@ pub fn build_app_with_config(
508510
.merge(model_routes)
509511
.merge(admin_routes)
510512
.merge(invitation_routes)
513+
.merge(auth_vpc_routes)
511514
.merge(files_routes)
512515
.merge(health_routes),
513516
)
514517
.merge(openapi_routes)
515518
}
516519

520+
/// Build VPC authentication routes
521+
pub fn build_auth_vpc_routes(app_state: AppState) -> Router {
522+
use crate::routes::auth_vpc::vpc_login;
523+
use axum::routing::post;
524+
525+
Router::new()
526+
.route("/auth/vpc/login", post(vpc_login))
527+
.with_state(app_state)
528+
}
529+
517530
/// Build invitation routes with selective auth
518531
pub fn build_invitation_routes(app_state: AppState, auth_state_middleware: &AuthState) -> Router {
519532
use crate::routes::users::{accept_invitation_by_token, get_invitation_by_token};

crates/api/src/routes/auth_vpc.rs

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
use crate::routes::api::AppState;
2+
use axum::{
3+
extract::State,
4+
http::StatusCode,
5+
response::{IntoResponse, Json},
6+
};
7+
use serde::{Deserialize, Serialize};
8+
use services::{
9+
auth::{ports::OAuthUserInfo, Session},
10+
organization::ports::Organization,
11+
workspace::ports::Workspace,
12+
};
13+
14+
#[derive(Debug, Deserialize)]
15+
pub struct VpcLoginRequest {
16+
pub timestamp: i64,
17+
pub signature: String,
18+
pub client_id: String,
19+
}
20+
21+
#[derive(Debug, Serialize, Deserialize)]
22+
pub struct VpcLoginResponse {
23+
pub access_token: String,
24+
pub session: Session,
25+
pub refresh_token: String,
26+
pub api_key: String,
27+
pub organization: Organization,
28+
pub workspace: Workspace,
29+
}
30+
31+
pub async fn vpc_login(
32+
State(state): State<AppState>,
33+
Json(payload): Json<VpcLoginRequest>,
34+
) -> Result<impl IntoResponse, (StatusCode, String)> {
35+
// Verify VPC signature
36+
let valid = state
37+
.attestation_service
38+
.verify_vpc_signature(payload.timestamp, payload.signature.clone())
39+
.await
40+
.map_err(|e| {
41+
tracing::error!("VPC signature verification failed: {e:?}");
42+
(
43+
StatusCode::INTERNAL_SERVER_ERROR,
44+
"Verification error".to_string(),
45+
)
46+
})?;
47+
48+
if !valid {
49+
return Err((
50+
StatusCode::UNAUTHORIZED,
51+
"Invalid VPC signature".to_string(),
52+
));
53+
}
54+
55+
// Get or create Chat API user with deterministic mapping
56+
let provider_user_id = format!("vpc:{}", payload.client_id);
57+
let email = format!("{}@vpc.internal.near.ai", payload.client_id);
58+
let username = payload.client_id.clone();
59+
60+
let user_info = OAuthUserInfo {
61+
provider: "vpc".to_string(),
62+
provider_user_id,
63+
email,
64+
username,
65+
display_name: None,
66+
avatar_url: None,
67+
};
68+
69+
let user = state
70+
.auth_service
71+
.get_or_create_oauth_user(user_info)
72+
.await
73+
.map_err(|e| {
74+
tracing::error!("Failed to get/create Chat API user: {e:?}");
75+
(
76+
StatusCode::INTERNAL_SERVER_ERROR,
77+
"User creation error".to_string(),
78+
)
79+
})?;
80+
81+
// Create session
82+
let (access_token, session, refresh_token) = state
83+
.auth_service
84+
.create_session(
85+
user.id.clone(),
86+
None,
87+
"VPC/1.0".to_string(),
88+
state.config.auth.encoding_key.clone(),
89+
24,
90+
24 * 30,
91+
)
92+
.await
93+
.map_err(|e| {
94+
tracing::error!("Failed to create session: {e:?}");
95+
(
96+
StatusCode::INTERNAL_SERVER_ERROR,
97+
"Session creation error".to_string(),
98+
)
99+
})?;
100+
101+
// Create unbound API key for this session
102+
// 1. Get default organization for user
103+
let orgs = state
104+
.organization_service
105+
.list_organizations_for_user(user.id.clone(), 1, 0, None, None)
106+
.await
107+
.map_err(|e| {
108+
tracing::error!("Failed to list organizations for user: {e:?}");
109+
(
110+
StatusCode::INTERNAL_SERVER_ERROR,
111+
"Organization lookup error".to_string(),
112+
)
113+
})?;
114+
115+
let org = orgs.first().ok_or_else(|| {
116+
tracing::error!("User has no organizations");
117+
(
118+
StatusCode::INTERNAL_SERVER_ERROR,
119+
"No organization found".to_string(),
120+
)
121+
})?;
122+
123+
// 2. Get default workspace for organization
124+
let workspaces = state
125+
.workspace_service
126+
.list_workspaces_for_organization(org.id.clone(), user.id.clone())
127+
.await
128+
.map_err(|e| {
129+
tracing::error!("Failed to list workspaces: {e:?}");
130+
(
131+
StatusCode::INTERNAL_SERVER_ERROR,
132+
"Workspace lookup error".to_string(),
133+
)
134+
})?;
135+
136+
let workspace = workspaces.first().ok_or_else(|| {
137+
tracing::error!("Organization has no workspaces");
138+
(
139+
StatusCode::INTERNAL_SERVER_ERROR,
140+
"No workspace found".to_string(),
141+
)
142+
})?;
143+
144+
// 3. Create API key
145+
let api_key_name = format!("VPC Key - {}", payload.client_id);
146+
let api_key = state
147+
.workspace_service
148+
.create_api_key(services::workspace::ports::CreateApiKeyRequest {
149+
name: api_key_name,
150+
workspace_id: workspace.id.clone(),
151+
created_by_user_id: user.id,
152+
expires_at: None, // Unbound expiry
153+
spend_limit: None, // Unbound spend limit
154+
})
155+
.await
156+
.map_err(|e| {
157+
tracing::error!("Failed to create API key: {e:?}");
158+
(
159+
StatusCode::INTERNAL_SERVER_ERROR,
160+
"API key creation error".to_string(),
161+
)
162+
})?;
163+
164+
let key = api_key.key.ok_or_else(|| {
165+
tracing::error!("API key was created but key value is missing");
166+
(
167+
StatusCode::INTERNAL_SERVER_ERROR,
168+
"API key creation error".to_string(),
169+
)
170+
})?;
171+
172+
Ok(Json(VpcLoginResponse {
173+
access_token,
174+
session,
175+
refresh_token,
176+
api_key: key,
177+
organization: org.clone(),
178+
workspace: workspace.clone(),
179+
}))
180+
}

crates/api/src/routes/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod admin;
22
pub mod api;
33
pub mod attestation;
44
pub mod auth;
5+
pub mod auth_vpc;
56
pub mod common;
67
pub mod completions;
78
pub mod conversations;

0 commit comments

Comments
 (0)