First JWT login implementation
parent
7a97b99bd6
commit
9309ec77fb
|
@ -1179,6 +1179,7 @@ checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
|
||||||
name = "rusty-bever"
|
name = "rusty-bever"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
|
|
|
@ -28,6 +28,7 @@ jwt = "0.14.0"
|
||||||
hmac = "*"
|
hmac = "*"
|
||||||
sha2 = "*"
|
sha2 = "*"
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
|
base64 = "0.13.0"
|
||||||
|
|
||||||
# Backend web framework
|
# Backend web framework
|
||||||
[dependencies.rocket]
|
[dependencies.rocket]
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
use crate::errors::RBError;
|
use crate::errors::RBError;
|
||||||
use crate::models::User;
|
use crate::models::{User, NewRefreshToken};
|
||||||
use crate::schema::users::dsl as users;
|
use crate::schema::users::dsl as users;
|
||||||
|
use crate::schema::refresh_tokens::dsl as refresh_tokens;
|
||||||
use argon2::verify_encoded;
|
use argon2::verify_encoded;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::PgConnection;
|
use diesel::{PgConnection, insert_into};
|
||||||
use hmac::{Hmac, NewMac};
|
use hmac::{Hmac, NewMac};
|
||||||
use jwt::SignWithKey;
|
use jwt::SignWithKey;
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use serde::Serialize;
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
|
||||||
/// Expire time for the JWT tokens in seconds.
|
/// Expire time for the JWT tokens in seconds.
|
||||||
const JWT_EXP_SECONDS: i64 = 900;
|
const JWT_EXP_SECONDS: i64 = 900;
|
||||||
/// Amount of bytes the refresh tokens should consist of
|
/// Amount of bytes the refresh tokens should consist of
|
||||||
const REFRESH_TOKEN_N_BYTES: u32 = 64;
|
const REFRESH_TOKEN_N_BYTES: usize = 64;
|
||||||
|
|
||||||
pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result<User> {
|
pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result<User> {
|
||||||
// TODO handle non-"NotFound" Diesel errors accordingely
|
// TODO handle non-"NotFound" Diesel errors accordingely
|
||||||
|
@ -28,23 +31,39 @@ pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct JWTResponse {
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct JWTResponse {
|
||||||
token: String,
|
token: String,
|
||||||
refresh_token: String
|
refresh_token: String
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> JWTResponse {
|
pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result<JWTResponse> {
|
||||||
// TODO actually use proper secret here
|
// TODO actually use proper secret here
|
||||||
// TODO don't just unwrap here
|
let key: Hmac<Sha256> = Hmac::new_from_slice(b"some-secret").map_err(|_| RBError::JWTCreationError)?;
|
||||||
let key: Hmac<Sha256> = Hmac::new_from_slice(b"some-secret").unwrap();
|
|
||||||
|
|
||||||
// Create the claims
|
// Create the claims
|
||||||
let mut claims = HashMap::new();
|
let mut claims = HashMap::new();
|
||||||
claims.insert("id", user.id.to_string());
|
claims.insert("id", user.id.to_string());
|
||||||
claims.insert("username", user.username);
|
claims.insert("username", user.username.clone());
|
||||||
|
claims.insert("admin", user.admin.to_string());
|
||||||
claims.insert("exp", (Utc::now().timestamp() + JWT_EXP_SECONDS).to_string());
|
claims.insert("exp", (Utc::now().timestamp() + JWT_EXP_SECONDS).to_string());
|
||||||
|
|
||||||
// Sign the claims into a new token
|
// Sign the claims into a new token
|
||||||
// TODO don't just unwrap here
|
let token = claims.sign_with_key(&key).map_err(|_| RBError::JWTCreationError)?;
|
||||||
let token = claims.sign_with_key(&key).unwrap();
|
|
||||||
|
// Generate a random refresh token
|
||||||
|
let mut refresh_token = [0u8; REFRESH_TOKEN_N_BYTES];
|
||||||
|
thread_rng().fill(&mut refresh_token[..]);
|
||||||
|
|
||||||
|
// Store refresh token in database
|
||||||
|
insert_into(refresh_tokens::refresh_tokens).values(NewRefreshToken {
|
||||||
|
token: refresh_token.to_vec(),
|
||||||
|
user_id: user.id
|
||||||
|
}).execute(conn).map_err(|_| RBError::JWTCreationError)?;
|
||||||
|
|
||||||
|
Ok(JWTResponse {
|
||||||
|
token: token,
|
||||||
|
refresh_token: base64::encode(refresh_token)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,19 +11,22 @@ pub enum RBError {
|
||||||
/// When a non-admin user tries to use an admin endpoint
|
/// When a non-admin user tries to use an admin endpoint
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
/// When an expired JWT token is used for auth.
|
/// When an expired JWT token is used for auth.
|
||||||
JWTTokenExpired
|
JWTTokenExpired,
|
||||||
|
/// Umbrella error for when something goes wrong whilst creating a JWT token pair
|
||||||
|
JWTCreationError
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'r> Responder<'r, 'static> for RBError {
|
impl<'r> Responder<'r, 'static> for RBError {
|
||||||
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
|
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
|
||||||
let (status, message): (Status, &str) = match self {
|
let (status, message): (Status, &str) = match self {
|
||||||
UnknownUser => (Status::NotFound, "Unknown user"),
|
RBError::UnknownUser => (Status::NotFound, "Unknown user"),
|
||||||
InvalidPassword => (Status::Unauthorized, "Invalid password"),
|
RBError::InvalidPassword => (Status::Unauthorized, "Invalid password"),
|
||||||
Unauthorized => (Status::Unauthorized, "Unauthorized"),
|
RBError::Unauthorized => (Status::Unauthorized, "Unauthorized"),
|
||||||
JWTTokenExpired => (Status::Unauthorized, "Token expired"),
|
RBError::JWTTokenExpired => (Status::Unauthorized, "Token expired"),
|
||||||
|
RBError::JWTCreationError => (Status::InternalServerError, "Failed to create tokens."),
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = Response::new();
|
let mut res = Response::new();
|
||||||
res.set_status(status);
|
res.set_status(status);
|
||||||
res.set_sized_body(message.len(), io::Cursor::new(message));
|
res.set_sized_body(message.len(), io::Cursor::new(message));
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use diesel::Queryable;
|
use diesel::{Queryable, Insertable};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use crate::schema::refresh_tokens;
|
||||||
|
|
||||||
#[derive(Queryable, Serialize)]
|
#[derive(Queryable, Serialize)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
|
@ -10,5 +11,13 @@ pub struct User {
|
||||||
pub password: String,
|
pub password: String,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
blocked: bool,
|
blocked: bool,
|
||||||
admin: bool,
|
pub admin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Insertable)]
|
||||||
|
#[table_name = "refresh_tokens"]
|
||||||
|
pub struct NewRefreshToken {
|
||||||
|
pub token: Vec<u8>,
|
||||||
|
pub user_id: Uuid
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::RbDbConn;
|
use crate::RbDbConn;
|
||||||
use rb::auth::verify_user;
|
use rb::auth::{verify_user, JWTResponse, generate_jwt_token};
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
@ -9,14 +9,18 @@ struct Credentials {
|
||||||
password: String,
|
password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO add catch for when user immediately requests new JWT token (they could totally spam this)
|
||||||
|
|
||||||
#[post("/login", data = "<credentials>")]
|
#[post("/login", data = "<credentials>")]
|
||||||
async fn login(conn: RbDbConn, credentials: Json<Credentials>) {
|
async fn login(conn: RbDbConn, credentials: Json<Credentials>) -> rb::Result<Json<JWTResponse>> {
|
||||||
let credentials = credentials.into_inner();
|
let credentials = credentials.into_inner();
|
||||||
|
|
||||||
|
// Get the user, if credentials are valid
|
||||||
let user = conn
|
let user = conn
|
||||||
.run(move |c| verify_user(c, &credentials.username, &credentials.password))
|
.run(move |c| verify_user(c, &credentials.username, &credentials.password))
|
||||||
.await;
|
.await?;
|
||||||
user
|
|
||||||
|
Ok(Json(conn.run(move |c| generate_jwt_token(c, &user)).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
// /refresh
|
// /refresh
|
||||||
|
|
Reference in New Issue