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
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)),
|
||||
))
|
||||
}
|
||||
|
|
Reference in New Issue