forked from Chewing_Bever/rusty-bever
First draft of token refresh
parent
badf68e579
commit
7dffbb9597
|
@ -15,7 +15,7 @@ CREATE TABLE refresh_tokens (
|
||||||
-- This is more efficient than storing the text
|
-- This is more efficient than storing the text
|
||||||
token bytea PRIMARY KEY,
|
token bytea PRIMARY KEY,
|
||||||
-- The user for whom the token was created
|
-- 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
|
-- When the token expires
|
||||||
expires_at timestamp NOT NULL,
|
expires_at timestamp NOT NULL,
|
||||||
-- When the token was last used (is NULL until used)
|
-- When the token was last used (is NULL until used)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::errors::RBError;
|
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::refresh_tokens::dsl as refresh_tokens;
|
||||||
use crate::schema::users::dsl as users;
|
use crate::schema::users::dsl as users;
|
||||||
use argon2::verify_encoded;
|
use argon2::verify_encoded;
|
||||||
|
@ -121,3 +121,36 @@ pub fn create_admin_user(
|
||||||
|
|
||||||
Ok(true)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,9 @@ pub enum RBError {
|
||||||
PWSaltError,
|
PWSaltError,
|
||||||
AdminCreationError,
|
AdminCreationError,
|
||||||
TokenExpired,
|
TokenExpired,
|
||||||
|
InvalidRefreshToken,
|
||||||
|
DuplicateRefreshToken,
|
||||||
|
DBError,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'r> Responder<'r, 'static> for RBError {
|
impl<'r> Responder<'r, 'static> for RBError {
|
||||||
|
@ -34,6 +37,7 @@ impl<'r> Responder<'r, 'static> for RBError {
|
||||||
RBError::JWTCreationError | RBError::MissingJWTKey => {
|
RBError::JWTCreationError | RBError::MissingJWTKey => {
|
||||||
(Status::InternalServerError, "Failed to create tokens.")
|
(Status::InternalServerError, "Failed to create tokens.")
|
||||||
}
|
}
|
||||||
|
RBError::InvalidRefreshToken | RBError::DuplicateRefreshToken => (Status::Unauthorized, "Invalid refresh token."),
|
||||||
_ => (Status::InternalServerError, "Internal server error"),
|
_ => (Status::InternalServerError, "Internal server error"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,14 @@ pub struct NewUser {
|
||||||
pub admin: bool,
|
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)]
|
#[derive(Insertable)]
|
||||||
#[table_name = "refresh_tokens"]
|
#[table_name = "refresh_tokens"]
|
||||||
pub struct NewRefreshToken {
|
pub struct NewRefreshToken {
|
||||||
|
|
|
@ -5,7 +5,7 @@ use rocket::serde::json::Json;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
pub(crate) fn routes() -> Vec<rocket::Route> {
|
pub(crate) fn routes() -> Vec<rocket::Route> {
|
||||||
routes![login, already_logged_in, me]
|
routes![login, already_logged_in, refresh_token]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[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?))
|
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)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue