diff --git a/native/cpp/CommonCpp/grpc/protos/identity.proto b/native/cpp/CommonCpp/grpc/protos/identity.proto index 9cb21b43e..a757ee7e9 100644 --- a/native/cpp/CommonCpp/grpc/protos/identity.proto +++ b/native/cpp/CommonCpp/grpc/protos/identity.proto @@ -1,118 +1,118 @@ syntax = "proto3"; package identity; service IdentityService { // Called by user to register with the Identity Service (PAKE only) rpc RegisterUser(stream RegistrationRequest) returns (stream RegistrationResponse) {} // Called by user to create an active session and get an access token rpc LoginUser(stream LoginRequest) returns (stream LoginResponse) {} // Called by other services to verify a user's token rpc VerifyUserToken(VerifyUserTokenRequest) returns (VerifyUserTokenResponse) {} } // Helper types message PakeRegistrationRequestAndUserID { string userID = 1; string deviceID = 2; // Message sent to initiate PAKE registration (step 1) bytes pakeRegistrationRequest = 3; } message PakeCredentialRequestAndUserID { string userID = 1; string deviceID = 2; // Message sent to initiate PAKE login (step 1) bytes pakeCredentialRequest = 3; } message PakeLoginRequest { oneof data { PakeCredentialRequestAndUserID pakeCredentialRequestAndUserID = 1; // Final message in PAKE login (step 3) bytes pakeCredentialFinalization = 2; } } message PakeLoginResponse { oneof data { // Answer sent to the user upon reception of the PAKE login attempt, // containing a sealed envelope with the user's private key (step 2) bytes pakeCredentialResponse = 1; - string token = 2; + string accessToken = 2; } } message PakeRegistrationUploadAndCredentialRequest { // Final message in PAKE registration, containing sealed cryptographic // identifiers (step 3) bytes pakeRegistrationUpload = 1; // Message sent to initiate PAKE login (Same as in // PakeCredentialRequestAndUserID) (step 1) bytes pakeCredentialRequest = 2; } message WalletLoginRequest { string userID = 1; string deviceID = 2; - string walletAddress = 3; - bytes signedMessage = 4; + string siweMessage = 3; + bytes siweSignature = 4; } message WalletLoginResponse { - bytes token = 1; + string accessToken = 1; } // RegisterUser message RegistrationRequest { oneof data { PakeRegistrationRequestAndUserID pakeRegistrationRequestAndUserID = 1; // We combine the last step of PAKE registration with the first step of PAKE // login here to reduce the number of messages sent PakeRegistrationUploadAndCredentialRequest pakeRegistrationUploadAndCredentialRequest = 2; // Final message in PAKE login (Same as in PakeLoginRequest) (step 3) bytes pakeCredentialFinalization = 3; } } message RegistrationResponse { oneof data { // Answer sent to the user upon reception of the PAKE registration attempt // (step 2) bytes pakeRegistrationResponse = 1; PakeLoginResponse pakeLoginResponse = 2; } } // LoginUser message LoginRequest { oneof data { PakeLoginRequest pakeLoginRequest = 1; WalletLoginRequest walletLoginRequest = 2; } } message LoginResponse { oneof data { PakeLoginResponse pakeLoginResponse = 1; WalletLoginResponse walletLoginResponse = 2; } } // VerifyUserToken message VerifyUserTokenRequest { string userID = 1; string deviceID = 2; - string token = 3; + string accessToken = 3; } message VerifyUserTokenResponse { bool tokenValid = 1; } diff --git a/services/identity/src/database.rs b/services/identity/src/database.rs index 13a3c4589..12c689a45 100644 --- a/services/identity/src/database.rs +++ b/services/identity/src/database.rs @@ -1,374 +1,374 @@ use std::collections::HashMap; use bytes::Bytes; use chrono::{DateTime, ParseError, Utc}; use opaque_ke::{errors::ProtocolError, ServerRegistration}; use rusoto_core::{Region, RusotoError}; use rusoto_dynamodb::{ AttributeValue, DynamoDb, DynamoDbClient, GetItemError, GetItemInput, GetItemOutput, PutItemError, PutItemInput, PutItemOutput, }; use tracing::{error, info}; use crate::opaque::Cipher; -use crate::token::{AccessToken, AuthType}; +use crate::token::{AccessTokenData, AuthType}; pub struct DatabaseClient { client: DynamoDbClient, } impl DatabaseClient { pub fn new(region: Region) -> Self { DatabaseClient { client: DynamoDbClient::new(region), } } pub async fn get_pake_registration( &self, user_id: String, ) -> Result>, Error> { let primary_key = create_simple_primary_key(("userID".to_string(), user_id.clone())); let get_item_input = GetItemInput { table_name: "identity-pake-registration".to_string(), key: primary_key, consistent_read: Some(true), ..GetItemInput::default() }; let get_item_result = self.client.get_item(get_item_input).await; match get_item_result { Ok(GetItemOutput { item: Some(item), .. }) => { if let Some(AttributeValue { b: Some(server_registration_bytes), .. }) = item.get("pakeRegistrationData") { match ServerRegistration::::deserialize( server_registration_bytes, ) { Ok(server_registration) => Ok(Some(server_registration)), Err(e) => { error!( "Failed to deserialize ServerRegistration struct for user {}: {}", user_id, e ); Err(Error::Pake(e)) } } } else { error!("No registration data found for registered user {}", user_id); Err(Error::MissingAttribute) } } Ok(_) => { info!( "No item found for user {} in PAKE registration table", user_id ); Ok(None) } Err(e) => { error!( "DynamoDB client failed to get registration data for user {}: {}", user_id, e ); Err(Error::RusotoGet(e)) } } } pub async fn put_pake_registration( &self, user_id: String, registration: ServerRegistration, ) -> Result> { let input = PutItemInput { table_name: "identity-pake-registration".to_string(), item: HashMap::from([ ( "userID".to_string(), AttributeValue { s: Some(user_id), ..Default::default() }, ), ( "pakeRegistrationData".to_string(), AttributeValue { b: Some(Bytes::from(registration.serialize())), ..Default::default() }, ), ]), ..PutItemInput::default() }; self.client.put_item(input).await } - pub async fn get_token( + pub async fn get_access_token_data( &self, user_id: String, device_id: String, - ) -> Result, Error> { + ) -> Result, Error> { let primary_key = create_composite_primary_key( ("userID".to_string(), user_id.clone()), ("deviceID".to_string(), device_id.clone()), ); let get_item_input = GetItemInput { table_name: "identity-tokens".to_string(), key: primary_key, consistent_read: Some(true), ..GetItemInput::default() }; let get_item_result = self.client.get_item(get_item_input).await; match get_item_result { Ok(GetItemOutput { item: Some(mut item), .. }) => { let created = parse_created_attribute(item.remove("created"))?; let auth_type = parse_auth_type_attribute(item.remove("authType"))?; let valid = parse_valid_attribute(item.remove("valid"))?; - let token = parse_token_attribute(item.remove("token"))?; - Ok(Some(AccessToken { + let access_token = parse_token_attribute(item.remove("token"))?; + Ok(Some(AccessTokenData { user_id, device_id, - token, + access_token, created, auth_type, valid, })) } Ok(_) => { info!( "No item found for user {} and device {} in token table", user_id, device_id ); Ok(None) } Err(e) => { error!( "DynamoDB client failed to get token for user {} on device {}: {}", user_id, device_id, e ); Err(Error::RusotoGet(e)) } } } - pub async fn put_token( + pub async fn put_access_token_data( &self, - token: AccessToken, + access_token_data: AccessTokenData, ) -> Result { let input = PutItemInput { table_name: "identity-tokens".to_string(), item: HashMap::from([ ( "userID".to_string(), AttributeValue { - s: Some(token.user_id), + s: Some(access_token_data.user_id), ..Default::default() }, ), ( "deviceID".to_string(), AttributeValue { - s: Some(token.device_id), + s: Some(access_token_data.device_id), ..Default::default() }, ), ( "token".to_string(), AttributeValue { - s: Some(token.token), + s: Some(access_token_data.access_token), ..Default::default() }, ), ( "created".to_string(), AttributeValue { - s: Some(token.created.to_rfc3339()), + s: Some(access_token_data.created.to_rfc3339()), ..Default::default() }, ), ( "authType".to_string(), AttributeValue { - s: Some(match token.auth_type { + s: Some(match access_token_data.auth_type { AuthType::Password => "password".to_string(), AuthType::Wallet => "wallet".to_string(), }), ..Default::default() }, ), ( "valid".to_string(), AttributeValue { - bool: Some(token.valid), + bool: Some(access_token_data.valid), ..Default::default() }, ), ]), ..PutItemInput::default() }; self.client.put_item(input).await.map_err(Error::RusotoPut) } } #[derive( Debug, derive_more::Display, derive_more::From, derive_more::Error, )] pub enum Error { #[display(...)] RusotoGet(RusotoError), #[display(...)] RusotoPut(RusotoError), #[display(...)] Pake(ProtocolError), #[display(...)] MissingAttribute, #[display(...)] InvalidTimestamp(ParseError), #[display(...)] InvalidAuthType, } type AttributeName = String; fn create_simple_primary_key( partition_key: (AttributeName, String), ) -> HashMap { HashMap::from([( partition_key.0, AttributeValue { s: Some(partition_key.1), ..Default::default() }, )]) } fn create_composite_primary_key( partition_key: (AttributeName, String), sort_key: (AttributeName, String), ) -> HashMap { let mut primary_key = create_simple_primary_key(partition_key); primary_key.insert( sort_key.0, AttributeValue { s: Some(sort_key.1), ..Default::default() }, ); primary_key } fn parse_created_attribute( attribute: Option, ) -> Result, Error> { if let Some(AttributeValue { s: Some(created), .. }) = attribute { created.parse().map_err(Error::InvalidTimestamp) } else { Err(Error::MissingAttribute) } } fn parse_auth_type_attribute( attribute: Option, ) -> Result { if let Some(AttributeValue { s: Some(auth_type), .. }) = attribute { match auth_type.as_str() { "password" => Ok(AuthType::Password), "wallet" => Ok(AuthType::Wallet), _ => Err(Error::InvalidAuthType), } } else { Err(Error::MissingAttribute) } } fn parse_valid_attribute( attribute: Option, ) -> Result { if let Some(AttributeValue { bool: Some(valid), .. }) = attribute { Ok(valid) } else { Err(Error::MissingAttribute) } } fn parse_token_attribute( attribute: Option, ) -> Result { if let Some(AttributeValue { s: Some(token), .. }) = attribute { Ok(token) } else { Err(Error::MissingAttribute) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_create_simple_primary_key() { let partition_key_name = "userID".to_string(); let partition_key_value = "12345".to_string(); let partition_key = (partition_key_name.clone(), partition_key_value.clone()); let mut primary_key = create_simple_primary_key(partition_key); assert_eq!(primary_key.len(), 1); let attribute = primary_key.remove(&partition_key_name); assert!(attribute.is_some()); assert_eq!( attribute, Some(AttributeValue { s: Some(partition_key_value), ..Default::default() }) ); } #[test] fn test_create_composite_primary_key() { let partition_key_name = "userID".to_string(); let partition_key_value = "12345".to_string(); let partition_key = (partition_key_name.clone(), partition_key_value.clone()); let sort_key_name = "deviceID".to_string(); let sort_key_value = "54321".to_string(); let sort_key = (sort_key_name.clone(), sort_key_value.clone()); let mut primary_key = create_composite_primary_key(partition_key, sort_key); assert_eq!(primary_key.len(), 2); let partition_key_attribute = primary_key.remove(&partition_key_name); assert!(partition_key_attribute.is_some()); assert_eq!( partition_key_attribute, Some(AttributeValue { s: Some(partition_key_value), ..Default::default() }) ); let sort_key_attribute = primary_key.remove(&sort_key_name); assert!(sort_key_attribute.is_some()); assert_eq!( sort_key_attribute, Some(AttributeValue { s: Some(sort_key_value), ..Default::default() }) ) } } diff --git a/services/identity/src/service.rs b/services/identity/src/service.rs index cb127f55e..0a5d0406c 100644 --- a/services/identity/src/service.rs +++ b/services/identity/src/service.rs @@ -1,91 +1,91 @@ use constant_time_eq::constant_time_eq; use futures_core::Stream; use rusoto_core::RusotoError; use rusoto_dynamodb::GetItemError; use std::pin::Pin; use tonic::{Request, Response, Status}; use tracing::{error, info, instrument}; use crate::database::DatabaseClient; use crate::{config::Config, database::Error}; pub use proto::identity_service_server::IdentityServiceServer; use proto::{ identity_service_server::IdentityService, LoginRequest, LoginResponse, RegistrationRequest, RegistrationResponse, VerifyUserTokenRequest, VerifyUserTokenResponse, }; mod proto { tonic::include_proto!("identity"); } #[derive(derive_more::Constructor)] pub struct MyIdentityService { config: Config, client: DatabaseClient, } #[tonic::async_trait] impl IdentityService for MyIdentityService { type RegisterUserStream = Pin< Box< dyn Stream> + Send + 'static, >, >; async fn register_user( &self, request: Request>, ) -> Result, Status> { println!("Got a registration request: {:?}", request); unimplemented!() } type LoginUserStream = Pin> + Send + 'static>>; async fn login_user( &self, request: Request>, ) -> Result, Status> { println!("Got a login request: {:?}", request); unimplemented!() } #[instrument(skip(self))] async fn verify_user_token( &self, request: Request, ) -> Result, Status> { info!("Received VerifyUserToken request: {:?}", request); let message = request.into_inner(); let token_valid = match self .client - .get_token(message.user_id, message.device_id) + .get_access_token_data(message.user_id, message.device_id) .await { - Ok(Some(access_token)) => constant_time_eq( - access_token.token.as_bytes(), - message.token.as_bytes(), + Ok(Some(access_token_data)) => constant_time_eq( + access_token_data.access_token.as_bytes(), + message.access_token.as_bytes(), ), Ok(None) => false, Err(Error::RusotoGet(RusotoError::Service( GetItemError::ResourceNotFound(_), ))) | Err(Error::RusotoGet(RusotoError::Credentials(_))) => { return Err(Status::failed_precondition("internal error")) } Err(Error::RusotoGet(_)) => { return Err(Status::unavailable("please retry")) } Err(e) => { error!("Encountered an unexpected error: {}", e); return Err(Status::failed_precondition("unexpected error")); } }; let response = Response::new(VerifyUserTokenResponse { token_valid }); info!("Sending VerifyUserToken response: {:?}", response); Ok(response) } } diff --git a/services/identity/src/token.rs b/services/identity/src/token.rs index aaf8c4564..0b7a17e5c 100644 --- a/services/identity/src/token.rs +++ b/services/identity/src/token.rs @@ -1,37 +1,37 @@ use chrono::{DateTime, Utc}; use rand::{ distributions::{Alphanumeric, DistString}, CryptoRng, Rng, }; pub enum AuthType { Password, Wallet, } -pub struct AccessToken { +pub struct AccessTokenData { pub user_id: String, pub device_id: String, - pub token: String, + pub access_token: String, pub created: DateTime, pub auth_type: AuthType, pub valid: bool, } -impl AccessToken { +impl AccessTokenData { pub fn new( user_id: String, device_id: String, auth_type: AuthType, rng: &mut (impl Rng + CryptoRng), ) -> Self { - AccessToken { + AccessTokenData { user_id, device_id, - token: Alphanumeric.sample_string(rng, 512), + access_token: Alphanumeric.sample_string(rng, 512), created: Utc::now(), auth_type, valid: true, } } }