First draft of token refresh

develop
Jef Roosens 2021-08-22 10:42:58 +02:00
parent badf68e579
commit 7dffbb9597
Signed by untrusted user: Jef Roosens
GPG Key ID: B580B976584B5F30
5 changed files with 64 additions and 4 deletions

View File

@ -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)

View File

@ -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<JWTResponse> {
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::<RefreshToken>(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::<User>(conn).map_err(|_| RBError::DBError)?;
generate_jwt_token(conn, &user)
}

View File

@ -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"),
};

View File

@ -22,6 +22,14 @@ pub struct NewUser {
pub admin: bool,
}
#[derive(Queryable)]
pub struct RefreshToken {
pub token: Vec<u8>,
pub user_id: Uuid,
pub expires_at: chrono::NaiveDateTime,
pub last_used_at: Option<chrono::NaiveDateTime>,
}
#[derive(Insertable)]
#[table_name = "refresh_tokens"]
pub struct NewRefreshToken {

View File

@ -5,7 +5,7 @@ use rocket::serde::json::Json;
use serde::Deserialize;
pub(crate) fn routes() -> Vec<rocket::Route> {
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<Credentials>) -> rb::Result<Jso
Ok(Json(conn.run(move |c| generate_jwt_token(c, &user)).await?))
}
// #[post("/refresh", data=)]
#[derive(Deserialize)]
struct RefreshTokenRequest {
pub refresh_token: String,
}
#[post("/refresh", data = "<refresh_token_request>")]
async fn refresh_token(
conn: RbDbConn,
refresh_token_request: Json<RefreshTokenRequest>,
) -> rb::Result<Json<JWTResponse>> {
let refresh_token = refresh_token_request.into_inner().refresh_token;
Ok(Json(
conn.run(move |c| rb::auth::refresh_token(c, &refresh_token)),
))
}