diff --git a/migrations/2021-08-20-110251_users-and-auth/up.sql b/migrations/2021-08-20-110251_users-and-auth/up.sql index 7ffd9f1..4341778 100644 --- a/migrations/2021-08-20-110251_users-and-auth/up.sql +++ b/migrations/2021-08-20-110251_users-and-auth/up.sql @@ -15,7 +15,7 @@ CREATE TABLE refresh_tokens ( -- This is more efficient than storing the text token bytea PRIMARY KEY, -- The user for whom the token was created - user_id uuid NOT NULL REFERENCES users(id), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- When the token expires expires_at timestamp NOT NULL, -- When the token was last used (is NULL until used) diff --git a/src/rb/auth.rs b/src/rb/auth.rs index 18d3753..36f5bb9 100644 --- a/src/rb/auth.rs +++ b/src/rb/auth.rs @@ -1,5 +1,5 @@ use crate::errors::RBError; -use crate::models::{NewRefreshToken, NewUser, User}; +use crate::models::{NewRefreshToken, NewUser, RefreshToken, User}; use crate::schema::refresh_tokens::dsl as refresh_tokens; use crate::schema::users::dsl as users; use argon2::verify_encoded; @@ -121,3 +121,36 @@ pub fn create_admin_user( Ok(true) } + +pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> crate::Result { + let token_bytes = base64::decode(refresh_token).map_err(|_| RBError::InvalidRefreshToken)?; + + // First, we request the token from the database to see if it's really a valid token + let token_entry = refresh_tokens::refresh_tokens + .filter(refresh_tokens::token.eq(token_bytes)) + .first::(conn) + .map_err(|_| RBError::InvalidRefreshToken)?; + + // If we see that the token has already been used before, we block the user. + if token_entry.last_used_at.is_some() { + let target = users::users.filter(users::id.eq(token_entry.user_id)); + diesel::update(target) + .set(users::blocked.eq(true)) + .execute(conn) + .map_err(|_| RBError::DBError)?; + + return Err(RBError::DuplicateRefreshToken); + } + + // We update the last_used_at value for the refresh token + let target = refresh_tokens::refresh_tokens.filter(refresh_tokens::token.eq(token_entry.token)); + diesel::update(target) + .set(refresh_tokens::last_used_at.eq(Utc::now().naive_utc())) + .execute(conn) + .map_err(|_| RBError::DBError)?; + + // Finally, we query the new user & generate a new token + let user = users::users.filter(users::id.eq(token_entry.user_id)).first::(conn).map_err(|_| RBError::DBError)?; + + generate_jwt_token(conn, &user) +} diff --git a/src/rb/errors.rs b/src/rb/errors.rs index 9d048f3..b5b01a9 100644 --- a/src/rb/errors.rs +++ b/src/rb/errors.rs @@ -21,6 +21,9 @@ pub enum RBError { PWSaltError, AdminCreationError, TokenExpired, + InvalidRefreshToken, + DuplicateRefreshToken, + DBError, } impl<'r> Responder<'r, 'static> for RBError { @@ -34,6 +37,7 @@ impl<'r> Responder<'r, 'static> for RBError { RBError::JWTCreationError | RBError::MissingJWTKey => { (Status::InternalServerError, "Failed to create tokens.") } + RBError::InvalidRefreshToken | RBError::DuplicateRefreshToken => (Status::Unauthorized, "Invalid refresh token."), _ => (Status::InternalServerError, "Internal server error"), }; diff --git a/src/rb/models.rs b/src/rb/models.rs index 2df5f89..81b34ca 100644 --- a/src/rb/models.rs +++ b/src/rb/models.rs @@ -22,6 +22,14 @@ pub struct NewUser { pub admin: bool, } +#[derive(Queryable)] +pub struct RefreshToken { + pub token: Vec, + pub user_id: Uuid, + pub expires_at: chrono::NaiveDateTime, + pub last_used_at: Option, +} + #[derive(Insertable)] #[table_name = "refresh_tokens"] pub struct NewRefreshToken { diff --git a/src/rbs/auth.rs b/src/rbs/auth.rs index 2ff7561..f9ffe55 100644 --- a/src/rbs/auth.rs +++ b/src/rbs/auth.rs @@ -5,7 +5,7 @@ use rocket::serde::json::Json; use serde::Deserialize; pub(crate) fn routes() -> Vec { - routes![login, already_logged_in, me] + routes![login, already_logged_in, refresh_token] } #[derive(Deserialize)] @@ -31,4 +31,19 @@ async fn login(conn: RbDbConn, credentials: Json) -> rb::Result, +) -> rb::Result> { + let refresh_token = refresh_token_request.into_inner().refresh_token; + + Ok(Json( + conn.run(move |c| rb::auth::refresh_token(c, &refresh_token)), + )) +}