Skip to content

Commit

Permalink
de_server: Implement request authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
Indy2222 committed Dec 10, 2022
1 parent e57a285 commit df2fa66
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 9 deletions.
16 changes: 16 additions & 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/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ categories.workspace = true

[dependencies]
actix-web = "4.2.1"
actix-web-httpauth = "0.8.0"
anyhow = "1.0.66"
base64 = "0.13.1"
env_logger = "0.10.0"
Expand Down
2 changes: 1 addition & 1 deletion crates/server/src/auth/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use super::{

/// Registers all authentication endpoints.
pub(super) fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(web::scope("/p/auth").service(sign_up).service(sign_in));
cfg.service(web::scope("/auth").service(sign_up).service(sign_in));
}

#[post("/sign-up")]
Expand Down
82 changes: 82 additions & 0 deletions crates/server/src/auth/middleware.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use std::future::{ready, Ready};

use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
error::ErrorUnauthorized,
http::header::Header,
web, Error, HttpMessage,
};
use actix_web_httpauth::headers::authorization::{Authorization, Bearer};
use futures_util::future::LocalBoxFuture;
use log::warn;

use super::token::Tokens;

pub struct AuthMiddlewareFactory;

impl<S, B> Transform<S, ServiceRequest> for AuthMiddlewareFactory
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = AuthMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;

fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(AuthMiddleware { service }))
}
}

pub struct AuthMiddleware<S> {
service: S,
}

impl<S, B> Service<ServiceRequest> for AuthMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

forward_ready!(service);

fn call(&self, req: ServiceRequest) -> Self::Future {
let tokens = req.app_data::<web::Data<Tokens>>().unwrap().as_ref();

match Authorization::<Bearer>::parse(&req) {
Ok(auth) => match tokens.decode(auth.as_ref().token()) {
Ok(claims) => {
let previous = req.extensions_mut().insert(claims);
assert!(previous.is_none());
}
Err(error) => {
warn!("JWT decoding error: {:?}", error);
return Box::pin(async move {
Err(ErrorUnauthorized(format!(
"Invalid Bearer token provided: {}",
error
)))
});
}
},
Err(error) => {
warn!("JWT extraction error: {:?}", error);
return Box::pin(async move {
Err(ErrorUnauthorized(
"Authorization header with Bearer token not provided.",
))
});
}
}

let fut = self.service.call(req);
Box::pin(async move { fut.await })
}
}
10 changes: 8 additions & 2 deletions crates/server/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ use actix_web::web;
use anyhow::{ensure, Context, Result};
use sqlx::{Pool, Sqlite};

pub use self::middleware::AuthMiddlewareFactory;
use self::{db::Users, token::Tokens};
use crate::conf;

mod db;
mod endpoints;
mod middleware;
pub mod model;
mod passwd;
mod token;
Expand Down Expand Up @@ -52,10 +54,14 @@ impl Auth {
})
}

/// Configure actix-web application.
pub fn configure(&self, cfg: &mut web::ServiceConfig) {
/// Configure root scope of the actix-web application.
pub fn configure_root(&self, cfg: &mut web::ServiceConfig) {
cfg.app_data(web::Data::new(self.tokens.clone()));
cfg.app_data(web::Data::new(self.users.clone()));
}

/// Configure public scope of the actix-web application.
pub fn configure_public(&self, cfg: &mut web::ServiceConfig) {
endpoints::configure(cfg);
}
}
20 changes: 18 additions & 2 deletions crates/server/src/auth/token.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use anyhow::{Context, Result};
use jsonwebtoken::{encode, get_current_timestamp, EncodingKey, Header};
use jsonwebtoken::{
decode, encode, get_current_timestamp, DecodingKey, EncodingKey, Header, Validation,
};
use serde::{Deserialize, Serialize};

const TOKEN_LIFETIME: u64 = 86400;
Expand All @@ -25,19 +27,31 @@ impl Claims {
#[derive(Clone)]
pub(super) struct Tokens {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
}

impl Tokens {
pub(super) fn new(secret: &str) -> Result<Self> {
let secret = base64::decode(secret).context("Failed to decode JWT secret")?;
let encoding_key = EncodingKey::from_secret(secret.as_ref());
Ok(Self { encoding_key })
let decoding_key = DecodingKey::from_secret(secret.as_ref());
Ok(Self {
encoding_key,
decoding_key,
})
}

/// Encodes a user ID into a new JWT.
pub(super) fn encode(&self, claims: &Claims) -> Result<String> {
encode(&Header::default(), &claims, &self.encoding_key).context("Failed to encode JWT")
}

/// Decodes and validates a JWT.
pub fn decode(&self, token: &str) -> Result<Claims> {
decode(token, &self.decoding_key, &Validation::default())
.context("Failed to decode JWT")
.map(|t| t.claims)
}
}

#[cfg(test)]
Expand All @@ -51,5 +65,7 @@ mod tests {
let token_a = tokens.encode(&Claims::standard("Indy")).unwrap();
let token_b = tokens.encode(&Claims::standard("Indy2")).unwrap();
assert_ne!(token_a, token_b);
assert_eq!(tokens.decode(&token_a).unwrap().sub, "Indy");
assert_eq!(tokens.decode(&token_b).unwrap().sub, "Indy2");
}
}
2 changes: 1 addition & 1 deletion crates/server/src/games/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use super::{

/// Registers all authentication endpoints.
pub(super) fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(web::scope("/a/games").service(create).service(list));
cfg.service(web::scope("/games").service(create).service(list));
}

#[post("/")]
Expand Down
12 changes: 9 additions & 3 deletions crates/server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use actix_web::{middleware::Logger, web, App, HttpServer};
use anyhow::{Context, Result};
use auth::Auth;
use auth::{Auth, AuthMiddlewareFactory};
use games::GamesService;
use log::info;
use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite};
Expand Down Expand Up @@ -44,11 +44,17 @@ async fn main() -> std::io::Result<()> {
let games = handle_error!(GamesService::setup(db_pool).await);

HttpServer::new(move || {
let public_scope = web::scope("/p").configure(|c| auth.configure_public(c));
let authenticated_scope = web::scope("/a")
.wrap(AuthMiddlewareFactory)
.configure(|c| games.configure(c));

App::new()
.wrap(Logger::default())
.app_data(json_cfg.clone())
.configure(|c| auth.configure(c))
.configure(|c| games.configure(c))
.configure(|c| auth.configure_root(c))
.service(public_scope)
.service(authenticated_scope)
})
.bind(("0.0.0.0", http_port))?
.run()
Expand Down
10 changes: 10 additions & 0 deletions docs/content/lobby/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ paths:
get:
summary: List games.
description: This endpoint returns list of all games on the server.
security:
- bearerAuth: []
responses:
"200":
description: Game listing.
Expand All @@ -79,6 +81,8 @@ paths:
This endpoint is used to crate a new game. The user automatically joins
the game. It is not possible to simultaneously be part of multiple
games.
security:
- bearerAuth: []
requestBody:
content:
application/json:
Expand All @@ -97,6 +101,12 @@ paths:
description: A different game with the same name already exists.

components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT

schemas:
token-response:
type: object
Expand Down

0 comments on commit df2fa66

Please sign in to comment.