Skip to content

Commit

Permalink
Phase 2, token-based auth
Browse files Browse the repository at this point in the history
It's now possible to auth/auth via HTTP POST, and then "attach" via websocket.
  • Loading branch information
rdaum committed Nov 3, 2023
1 parent 81b9abe commit 6dd62aa
Show file tree
Hide file tree
Showing 9 changed files with 463 additions and 253 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/daemon/src/connections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub trait ConnectionsDB {
&self,
client_id: Uuid,
hostname: String,
player: Option<Objid>,
) -> Result<Objid, RpcRequestError>;

/// Record activity for the given client.
Expand Down
24 changes: 15 additions & 9 deletions crates/daemon/src/connections_tb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,18 @@ impl ConnectionsDB for ConnectionsTb {
&self,
client_id: Uuid,
hostname: String,
player: Option<Objid>,
) -> Result<Objid, RpcRequestError> {
// The connection object is pulled from the sequence, then we invert it and subtract from
// -4 to get the connection object, since they always grow downwards from there.
let connection_id = self.tb.clone().sequence_next(0).await;
let connection_id: i64 = -4 - (connection_id as i64);
let connection_oid = Objid(connection_id);
let connection_oid = match player {
None => {
// The connection object is pulled from the sequence, then we invert it and subtract from
// -4 to get the connection object, since they always grow downwards from there.
let connection_id = self.tb.clone().sequence_next(0).await;
let connection_id: i64 = -4 - (connection_id as i64);
Objid(connection_id)
}
Some(player) => player,
};

// Insert the initial tuples for the connection.
let tx = self.tb.clone().start_tx();
Expand Down Expand Up @@ -477,7 +483,7 @@ mod tests {
jh.push(tokio::spawn(async move {
let client_id = uuid::Uuid::new_v4();
let oid = db
.new_connection(client_id, "localhost".to_string())
.new_connection(client_id, "localhost".to_string(), None)
.await
.unwrap();
let client_ids = db.client_ids_for(oid).await.unwrap();
Expand Down Expand Up @@ -522,11 +528,11 @@ mod tests {
let client_id1 = uuid::Uuid::new_v4();
let client_id2 = uuid::Uuid::new_v4();
let con_oid1 = db
.new_connection(client_id1, "localhost".to_string())
.new_connection(client_id1, "localhost".to_string(), None)
.await
.unwrap();
let con_oid2 = db
.new_connection(client_id2, "localhost".to_string())
.new_connection(client_id2, "localhost".to_string(), None)
.await
.unwrap();
db.update_client_connection(con_oid1, Objid(x))
Expand Down Expand Up @@ -571,7 +577,7 @@ mod tests {
let db = Arc::new(ConnectionsTb::new(None).await);
let client_id1 = uuid::Uuid::new_v4();
let ob = db
.new_connection(client_id1, "localhost".to_string())
.new_connection(client_id1, "localhost".to_string(), None)
.await
.unwrap();
db.ping_check().await;
Expand Down
102 changes: 72 additions & 30 deletions crates/daemon/src/rpc_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,10 @@ use moor_values::SYSTEM_OBJECT;
use rpc_common::RpcResponse::{LoginResult, NewConnection};
use rpc_common::{
AuthToken, BroadcastEvent, ClientToken, ConnectType, ConnectionEvent, RpcRequest,
RpcRequestError, RpcResponse, BROADCAST_TOPIC,
RpcRequestError, RpcResponse, BROADCAST_TOPIC, MOOR_AUTH_TOKEN_FOOTER,
MOOR_SESSION_TOKEN_FOOTER,
};

pub const MOOR_SESSION_TOKEN_FOOTER: &str = "key-id:moor_rpc";
pub const MOOR_AUTH_TOKEN_FOOTER: &str = "key-id:moor_player";

use crate::connections::ConnectionsDB;
use crate::connections_tb::ConnectionsTb;
use crate::make_response;
Expand Down Expand Up @@ -99,14 +97,48 @@ impl RpcServer {
RpcRequest::ConnectionEstablish(hostname) => {
increment_counter!("rpc_server.connection_establish");

match self.connections.new_connection(client_id, hostname).await {
match self
.connections
.new_connection(client_id, hostname, None)
.await
{
Ok(oid) => {
let token = self.make_client_token(client_id);
make_response(Ok(NewConnection(token, oid)))
}
Err(e) => make_response(Err(e)),
}
}
RpcRequest::Attach(auth_token, connect_type, hostname) => {
// TODO: use _connect_type to provoke the
increment_counter!("rpc_server.attach");
// Validate the auth token, and get the player.
let Ok(player) = self.validate_auth_token(auth_token, None) else {
warn!("Invalid auth token for attach request");
return make_response(Err(RpcRequestError::PermissionDenied));
};
let client_token = match self
.connections
.new_connection(client_id, hostname, Some(player))
.await
{
Ok(_) => self.make_client_token(client_id),
Err(e) => return make_response(Err(e)),
};

trace!(?player, "Submitting user_connected task");
if let Err(e) = self
.clone()
.submit_connected_task(client_id, player, connect_type)
.await
{
error!(error = ?e, "Error submitting user_connected task");
increment_counter!("rpc_server.perform_login.submit_connected_task_failed");
// Note we still continue to return a successful login result here, hoping for the best
// but we do log the error.
}
make_response(Ok(RpcResponse::AttachResult(Some((client_token, player)))))
}
RpcRequest::Pong(token, _client_sys_time) => {
// Always respond with a ThanksPong, even if it's somebody we don't know.
// Can easily be a connection that was in the middle of negotiation at the time the
Expand Down Expand Up @@ -204,7 +236,7 @@ impl RpcServer {
return make_response(Err(RpcRequestError::PermissionDenied));
};

let Ok(_) = self.validate_auth_token(auth_token, connection) else {
let Ok(_) = self.validate_auth_token(auth_token, Some(connection)) else {
warn!(
?client_id,
?connection,
Expand Down Expand Up @@ -237,7 +269,7 @@ impl RpcServer {
return make_response(Err(RpcRequestError::PermissionDenied));
};

let Ok(_) = self.validate_auth_token(auth_token, connection) else {
let Ok(_) = self.validate_auth_token(auth_token, Some(connection)) else {
warn!(
?client_id,
?connection,
Expand Down Expand Up @@ -270,7 +302,7 @@ impl RpcServer {
return make_response(Err(RpcRequestError::PermissionDenied));
};

let Ok(_) = self.validate_auth_token(auth_token, connection) else {
let Ok(_) = self.validate_auth_token(auth_token, Some(connection)) else {
warn!(
?client_id,
?connection,
Expand Down Expand Up @@ -305,7 +337,7 @@ impl RpcServer {
return make_response(Err(RpcRequestError::PermissionDenied));
};

let Ok(_) = self.validate_auth_token(auth_token, connection) else {
let Ok(_) = self.validate_auth_token(auth_token, Some(connection)) else {
warn!(
?client_id,
?connection,
Expand All @@ -318,7 +350,7 @@ impl RpcServer {
RpcRequest::Detach(token) => {
increment_counter!("rpc_server.detach");

let Ok(connection) = self.validate_client_token(token, client_id) else {
let Ok(_) = self.validate_client_token(token, client_id) else {
warn!(?client_id, "Client token validation failed for request");
return make_response(Err(RpcRequestError::PermissionDenied));
};
Expand Down Expand Up @@ -532,8 +564,6 @@ impl RpcServer {
));
};

// Issue calls to user_connected/user_reconnected/user_created
// TODO: Reconnected/created
trace!(?player, "Submitting user_connected task");
if let Err(e) = self
.clone()
Expand Down Expand Up @@ -926,7 +956,7 @@ impl RpcServer {
let token = paseto::tokens::PasetoBuilder::new()
.set_ed25519_key(&self.keypair)
.set_issued_at(None)
.set_claim("player", json!(oid.0.to_string()))
.set_claim("player", json!(oid.0))
.set_issuer("moor")
.set_audience("moor_credentials")
.set_footer(MOOR_AUTH_TOKEN_FOOTER)
Expand All @@ -935,8 +965,8 @@ impl RpcServer {
AuthToken(token)
}

/// Validate the provided PASETO token against the provided client id and (optionally) player
/// objid. If they do not match, the request is rejected, permissions denied.
/// Validate the provided PASETO token against the provided client id
/// If they do not match, the request is rejected, permissions denied.
fn validate_client_token(
&self,
token: ClientToken,
Expand Down Expand Up @@ -988,11 +1018,17 @@ impl RpcServer {
Ok(())
}

/// Validate that the provided PASETO token is valid for the provided player Objid.
/// Validate that the provided PASETO token is valid.
/// If a player id is provided, validate it matches the player id.
/// Return the player id if it is valid.
/// Note that this is merely validating that the token is valid, not that the actual player
/// inside the token is valid and has the capapilities it thinks it has. That must be done in
/// the server.
fn validate_auth_token(&self, token: AuthToken, objid: Objid) -> Result<(), SessionError> {
/// inside the token is valid and has the capabilities it thinks it has. That must be done in
/// the runtime itself.
fn validate_auth_token(
&self,
token: AuthToken,
objid: Option<Objid>,
) -> Result<Objid, SessionError> {
let pk = paseto::tokens::PasetoPublicKey::ED25519KeyPair(&self.keypair);
let verified_token = paseto::tokens::validate_public_token(
&token.0,
Expand All @@ -1005,23 +1041,29 @@ impl RpcServer {
SessionError::InvalidToken
})?;

// Does the 'player' match objid? If not, reject it.
let Some(token_player) = verified_token.get("player") else {
debug!("Token does not contain player");
let (Some(Some("moor")), Some(Some("moor_credentials"))) = (
verified_token.get("iss").map(|s| s.as_str()),
verified_token.get("aud").map(|s| s.as_str()),
) else {
debug!("Token does not contain valid issuer/audience");
return Err(SessionError::InvalidToken);
};
let Some(token_player) = token_player.as_str() else {
debug!("Token player is not a string");

let Some(token_player) = verified_token.get("player") else {
debug!("Token does not contain player");
return Err(SessionError::InvalidToken);
};
let Ok(token_player) = token_player.parse() else {
debug!("Token player is not a valid Objid");
let Some(token_player) = token_player.as_i64() else {
debug!("Token player is not valid");
return Err(SessionError::InvalidToken);
};
let token_player = Objid(token_player);
if objid != token_player {
debug!(?objid, ?token_player, "Token player does not match objid");
return Err(SessionError::InvalidToken);
if let Some(objid) = objid {
// Does the 'player' match objid? If not, reject it.
if objid != token_player {
debug!(?objid, ?token_player, "Token player does not match objid");
return Err(SessionError::InvalidToken);
}
}

// TODO: we will need to verify that the player object id inside the token is valid inside
Expand All @@ -1030,7 +1072,7 @@ impl RpcServer {
// code with checks to make sure that the player objid is valid before letting it go
// forwards.

Ok(())
Ok(token_player)
}
}

Expand Down
19 changes: 18 additions & 1 deletion crates/rpc-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ use thiserror::Error;

pub const BROADCAST_TOPIC: &[u8; 9] = b"broadcast";

pub const MOOR_SESSION_TOKEN_FOOTER: &str = "key-id:moor_rpc";
pub const MOOR_AUTH_TOKEN_FOOTER: &str = "key-id:moor_player";

/// Errors at the RPC transport / encoding layer.
#[derive(Debug, thiserror::Error)]
#[derive(Debug, Error)]
pub enum RpcError {
#[error("could not send RPC request: {0}")]
CouldNotSend(String),
Expand All @@ -32,14 +35,27 @@ pub struct AuthToken(pub String);

#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)]
pub enum RpcRequest {
/// Establish a new connection, requesting a client token and a connection object
ConnectionEstablish(String),
/// Anonymously request a sysprop (e.g. $login.welcome_message)
RequestSysProp(ClientToken, String, String),
/// Login using the words (e.g. "create player bob" or "connect player bob") and return an
/// auth token and the object id of the player. None if the login failed.
LoginCommand(ClientToken, Vec<String>),
/// Attach to a previously-authenticated session, returning the object id of the player,
/// and a client token -- or None if the auth token is not valid.
Attach(AuthToken, ConnectType, String),
/// Send a command to be executed.
Command(ClientToken, AuthToken, String),
/// Respond to a request for input.
RequestedInput(ClientToken, AuthToken, u128, String),
/// Send an "out of band" command to be executed.
OutOfBand(ClientToken, AuthToken, String),
/// Evaluate a MOO expression.
Eval(ClientToken, AuthToken, String),
/// Respond to a ping request.
Pong(ClientToken, SystemTime),
/// We're done with this connection, buh-bye.
Detach(ClientToken),
}

Expand All @@ -62,6 +78,7 @@ pub enum RpcResponse {
NewConnection(ClientToken, Objid),
SysPropValue(Option<Var>),
LoginResult(Option<(AuthToken, ConnectType, Objid)>),
AttachResult(Option<(ClientToken, Objid)>),
CommandSubmitted(usize /* task id */),
InputThanks,
EvalResult(Var),
Expand Down
1 change: 1 addition & 0 deletions crates/web-host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ futures.workspace = true
futures-util.workspace = true
async-trait.workspace = true
bincode.workspace = true
thiserror.workspace = true

## Asynchronous transaction processing & networking
tokio.workspace = true
Expand Down
10 changes: 6 additions & 4 deletions crates/web-host/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod ws_connection;
mod ws_host;

use crate::ws_host::{auth_handler, WebSocketHost};
use crate::ws_host::WebSocketHost;
use anyhow::Context;
use axum::routing::{get, post};
use axum::Router;
Expand Down Expand Up @@ -50,9 +51,10 @@ fn mk_routes(ws_host: WebSocketHost) -> anyhow::Result<Router> {
let recorder_handle = setup_metrics_recorder()?;

let websocket_router = Router::new()
.route("/connect", get(ws_host::ws_connect_handler))
.route("/create", get(ws_host::ws_create_handler))
.route("/auth/:player", post(auth_handler))
.route("/attach/connect", get(ws_host::ws_connect_attach_handler))
.route("/attach/create", get(ws_host::ws_create_attach_handler))
.route("/auth/connect/:player", post(ws_host::connect_auth_handler))
.route("/auth/create/:player", post(ws_host::create_auth_handler))
.with_state(ws_host);

Ok(Router::new()
Expand Down
Loading

0 comments on commit 6dd62aa

Please sign in to comment.