diff --git a/services/identity/Dockerfile b/services/identity/Dockerfile --- a/services/identity/Dockerfile +++ b/services/identity/Dockerfile @@ -13,10 +13,16 @@ RUN mkdir -p /home/comm/app/identity WORKDIR /home/comm/app/identity +RUN cargo init --bin + +COPY services/identity/Cargo.toml services/identity/Cargo.lock ./ +COPY shared/ ../../shared/ + +# Cache build dependencies in a new layer +RUN cargo build --release +RUN rm src/*.rs COPY services/identity . -COPY shared/protos/identity_client.proto ../../shared/protos/ -COPY shared/comm-opaque2 ../../shared/comm-opaque2 RUN cargo install --locked --path . diff --git a/services/identity/build.rs b/services/identity/build.rs --- a/services/identity/build.rs +++ b/services/identity/build.rs @@ -3,7 +3,10 @@ .build_server(true) .build_client(false) .compile( - &["../../shared/protos/identity_client.proto"], + &[ + "../../shared/protos/identity_client.proto", + "../../shared/protos/identity_authenticated.proto", + ], &["../../shared/protos/"], )?; Ok(()) diff --git a/services/identity/src/grpc_services/authenticated.rs b/services/identity/src/grpc_services/authenticated.rs new file mode 100644 --- /dev/null +++ b/services/identity/src/grpc_services/authenticated.rs @@ -0,0 +1,79 @@ +use crate::{client_service::handle_db_error, database::DatabaseClient}; +use tonic::{Request, Response, Status}; + +// This must be named client, because generated code from the authenticated +// protobuf file references message structs from the client protobuf file +// with the client:: namespace +pub mod client { + tonic::include_proto!("identity.client"); +} +pub mod auth_proto { + tonic::include_proto!("identity.authenticated"); +} +use auth_proto::{ + identity_client_service_server::IdentityClientService, + RefreshUserPreKeysRequest, +}; +use client::Empty; +use tracing::debug; + +#[derive(derive_more::Constructor)] +pub struct AuthenticatedService { + db_client: DatabaseClient, +} + +fn get_value(req: &Request, key: &str) -> Option { + let raw_value = req.metadata().get(key)?; + raw_value.to_str().ok().map(|s| s.to_string()) +} + +fn get_auth_info(req: &Request<()>) -> Option<(String, String, String)> { + debug!("Retrieving auth info for request: {:?}", req); + + let user_id = get_value(req, "user_id")?; + let device_id = get_value(req, "device_id")?; + let access_token = get_value(req, "access_token")?; + + Some((user_id, device_id, access_token)) +} + +pub fn auth_intercept( + req: Request<()>, + db_client: &DatabaseClient, +) -> Result, Status> { + println!("Intercepting request: {:?}", req); + + let (user_id, device_id, access_token) = get_auth_info(&req) + .ok_or(Status::unauthenticated("Missing credentials"))?; + + let handle = tokio::runtime::Handle::current(); + let new_db_client = db_client.clone(); + + // This function cannot be `async`, yet must call the async db call + // Force tokio to resolve future in current thread without an explicit .await + let valid_token = tokio::task::block_in_place(move || { + handle + .block_on(new_db_client.verify_access_token( + user_id, + device_id, + access_token, + )) + .map_err(handle_db_error) + })?; + + if !valid_token { + return Err(Status::aborted("Bad Credentials")); + } + + Ok(req) +} + +#[tonic::async_trait] +impl IdentityClientService for AuthenticatedService { + async fn refresh_user_pre_keys( + &self, + _request: Request, + ) -> Result, Status> { + unimplemented!(); + } +} diff --git a/services/identity/src/grpc_services/mod.rs b/services/identity/src/grpc_services/mod.rs new file mode 100644 --- /dev/null +++ b/services/identity/src/grpc_services/mod.rs @@ -0,0 +1 @@ +pub mod authenticated; diff --git a/services/identity/src/main.rs b/services/identity/src/main.rs --- a/services/identity/src/main.rs +++ b/services/identity/src/main.rs @@ -9,6 +9,7 @@ mod config; pub mod constants; mod database; +mod grpc_services; mod id; mod keygen; mod nonce; @@ -23,6 +24,8 @@ use tracing_subscriber::EnvFilter; use client_service::{ClientService, IdentityClientServiceServer}; +use grpc_services::authenticated::auth_proto::identity_client_service_server::IdentityClientServiceServer as AuthServer; +use grpc_services::authenticated::AuthenticatedService; #[derive(Parser)] #[clap(author, version, about, long_about = None)] @@ -70,12 +73,19 @@ .time_to_live(Duration::from_secs(10)) .build(); let client_service = IdentityClientServiceServer::new( - ClientService::new(database_client, workflow_cache), + ClientService::new(database_client.clone(), workflow_cache), ); + let raw_auth_service = AuthenticatedService::new(database_client.clone()); + let auth_service = + AuthServer::with_interceptor(raw_auth_service, move |mut req| { + grpc_services::authenticated::auth_intercept(req, &database_client) + }); + info!("Listening to gRPC traffic on {}", addr); Server::builder() .accept_http1(true) .add_service(tonic_web::enable(client_service)) + .add_service(auth_service) .serve(addr) .await?; } diff --git a/shared/protos/identity_authenticated.proto b/shared/protos/identity_authenticated.proto new file mode 100644 --- /dev/null +++ b/shared/protos/identity_authenticated.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +import "identity_client.proto"; + +package identity.authenticated; + +// RPCs from a client (iOS, Android, or web) to identity service +// +// This service will assert authenticity of a device by verifying the access +// token through an interceptor, thus avoiding the need to explicitly pass +// the credentials on every request +service IdentityClientService { + + // Rotate a devices preKey and preKey signature + // Rotated for deniability of older messages + rpc RefreshUserPreKeys(RefreshUserPreKeysRequest) + returns (identity.client.Empty) {} +} + +// Helper types + +// RefreshUserPreKeys + +message RefreshUserPreKeysRequest { + identity.client.PreKey newContentPreKeys = 1; + identity.client.PreKey newNotifPreKeys = 2; +}