Skip to content

Commit

Permalink
Make auth tokens persistent (#16)
Browse files Browse the repository at this point in the history
* remove expiration time for auth tokens

Before, auth tokens expired after a week. After this change, they are valid until they are explicitly regenerated.

* improve keybind generator design
  • Loading branch information
dominikks authored Dec 2, 2023
1 parent 6062445 commit deafc5a
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 102 deletions.
133 changes: 82 additions & 51 deletions backend/src/api/auth.rs
Original file line number Diff line number Diff line change
@@ -1,47 +1,37 @@
use crate::api::utils::AvatarOrDefault;
use crate::api::Snowflake;
use crate::api::DISCORD_CLIENT_ID;
use crate::api::DISCORD_CLIENT_SECRET;
use crate::db::models;
use crate::db::DbConn;
use crate::discord::management::get_guilds_for_user;
use crate::discord::management::UserPermission;
use crate::CacheHttp;
use crate::BASE_URL;
use std::error::Error;
use std::iter;
use std::time::{Duration, SystemTime};

use bigdecimal::BigDecimal;
use bigdecimal::FromPrimitive;
use bigdecimal::ToPrimitive;
use diesel::prelude::*;
use diesel::result::Error as DieselError;
use oauth2::basic::BasicClient;
use oauth2::reqwest::async_http_client;
use oauth2::AuthUrl;
use oauth2::AuthorizationCode;
use oauth2::ClientId;
use oauth2::ClientSecret;
use oauth2::CsrfToken;
use oauth2::PkceCodeChallenge;
use oauth2::PkceCodeVerifier;
use oauth2::RedirectUrl;
use oauth2::RequestTokenError;
use oauth2::Scope;
use oauth2::TokenResponse;
use oauth2::TokenUrl;
use oauth2::{
AuthUrl, AuthorizationCode, CsrfToken, PkceCodeChallenge, PkceCodeVerifier, Scope,
TokenResponse,
};
use rand::distributions::Alphanumeric;
use rand::rngs::OsRng;
use rand::Rng;
use rocket::get;
use rocket::http::Cookie;
use rocket::http::CookieJar;
use rocket::http::SameSite;
use rocket::http::Status;
use rocket::http::{Cookie, SameSite};
use rocket::outcome::try_outcome;
use rocket::outcome::IntoOutcome;
use rocket::request;
use rocket::request::FromRequest;
use rocket::request::Outcome;
use rocket::response::status;
use rocket::response::Redirect;
use rocket::response::Responder;
use rocket::response::{status, Redirect};
use rocket::serde::json::Json;
use rocket::time::OffsetDateTime;
use rocket::Request;
Expand All @@ -53,10 +43,17 @@ use serde::Serializer;
use serde_with::serde_as;
use serde_with::TimestampSeconds;
use serenity::model::id::UserId as SerenityUserId;
use std::error::Error;
use std::iter;
use std::time::Duration;
use std::time::SystemTime;

use crate::api::utils::AvatarOrDefault;
use crate::api::Snowflake;
use crate::api::DISCORD_CLIENT_ID;
use crate::api::DISCORD_CLIENT_SECRET;
use crate::db::models;
use crate::db::DbConn;
use crate::discord::management::get_guilds_for_user;
use crate::discord::management::UserPermission;
use crate::CacheHttp;
use crate::BASE_URL;

static SESSION_COOKIE: &str = "auth_session";
static LOGIN_COOKIE: &str = "auth_login";
Expand Down Expand Up @@ -85,6 +82,7 @@ pub fn get_routes() -> Vec<Route> {
login_post,
login_error,
logout,
create_auth_token,
get_auth_token
]
}
Expand Down Expand Up @@ -158,6 +156,7 @@ impl<'r> FromRequest<'r> for TokenUserId {
type Error = ();

/// Protected api endpoints can inject `TokenUserId` to be accessible via auth token or session.
/// Tokens are valid indefinitely.
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
const TOKEN_PREFIX: &str = "Bearer ";
let db = try_outcome!(request.guard::<DbConn>().await);
Expand All @@ -178,19 +177,6 @@ impl<'r> FromRequest<'r> for TokenUserId {
})
.await
.ok()
.and_then(|auth_token| {
let diff = SystemTime::now()
.duration_since(auth_token.creation_time)
.ok()?
.as_secs();

// Ignore the token if it is more than a week old
if diff > 60 * 60 * 24 * 7 {
None
} else {
Some(auth_token)
}
})
.and_then(|auth_token| auth_token.user_id.to_u64())
.map(TokenUserId);

Expand All @@ -213,6 +199,8 @@ enum AuthError {
CsrfMissmatch(String),
#[response(status = 403)]
MissingLoginCookie(String),
#[response(status = 404)]
NotFound(String),
#[response(status = 500)]
RequestTokenError(String),
#[response(status = 500)]
Expand Down Expand Up @@ -243,8 +231,8 @@ impl From<reqwest::Error> for AuthError {
}
}

impl From<diesel::result::Error> for AuthError {
fn from(err: diesel::result::Error) -> Self {
impl From<DieselError> for AuthError {
fn from(err: DieselError) -> Self {
error!(?err, "Diesel error in API call");
Self::InternalError(String::from("Database operation failed."))
}
Expand Down Expand Up @@ -457,34 +445,77 @@ fn logout(cookies: &CookieJar<'_>) -> String {
String::from("User logged out")
}

#[serde_as]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct AuthToken {
token: String,
#[serde_as(as = "TimestampSeconds<String>")]
created_at: SystemTime,
}

impl From<models::AuthToken> for AuthToken {
fn from(value: models::AuthToken) -> Self {
Self {
token: value.token,
created_at: value.creation_time,
}
}
}

/// Beware: this replaces the current auth token with a new one. The old one becomes invalid.
#[post("/auth/gettoken")]
async fn get_auth_token(user: UserId, db: DbConn) -> Result<String, AuthError> {
#[post("/auth/token")]
async fn create_auth_token(user: UserId, db: DbConn) -> Result<Json<AuthToken>, AuthError> {
let uid = BigDecimal::from_u64(user.0).ok_or_else(AuthError::bigdecimal_error)?;
let auth_token: String = iter::repeat(())

let random_token: String = iter::repeat(())
.map(|_| OsRng.sample(Alphanumeric))
.map(char::from)
.take(32)
.collect();

let auth_token = models::AuthToken {
user_id: uid,
token: random_token.clone(),
creation_time: SystemTime::now(),
};

{
let auth_token = models::AuthToken {
user_id: uid,
token: auth_token.clone(),
creation_time: SystemTime::now(),
};
let auth_token = auth_token.clone();
db.run(move |c| {
use crate::db::schema::authtokens::dsl::*;

diesel::insert_into(authtokens)
.values(&auth_token.clone())
.values(&auth_token)
.on_conflict(user_id)
.do_update()
.set(auth_token)
.set(&auth_token)
.execute(c)
})
.await?;
}

Ok(auth_token)
Ok(Json(auth_token.into()))
}

#[get("/auth/token")]
async fn get_auth_token(user: UserId, db: DbConn) -> Result<Json<AuthToken>, AuthError> {
let uid = BigDecimal::from_u64(user.0).ok_or_else(AuthError::bigdecimal_error)?;

let token = db
.run(move |c| {
use crate::db::schema::authtokens::dsl::*;

authtokens.find(uid).first::<models::AuthToken>(c)
})
.await
.map_err(|err| {
if err == DieselError::NotFound {
AuthError::NotFound(String::from("No auth token found"))
} else {
AuthError::from(err)
}
})?;

Ok(Json(token.into()))
}
36 changes: 19 additions & 17 deletions backend/src/api/sounds.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
use crate::api::auth::UserId;
use crate::api::Snowflake;
use crate::audio_utils;
use crate::db::models;
use crate::db::DbConn;
use crate::discord::management::check_guild_moderator;
use crate::discord::management::check_guild_user;
use crate::discord::management::get_guilds_for_user;
use crate::discord::management::PermissionError;
use crate::file_handling;
use crate::CacheHttp;
use std::convert::TryFrom;
use std::num::TryFromIntError;
use std::path::PathBuf;
use std::time::SystemTime;

use bigdecimal::BigDecimal;
use bigdecimal::FromPrimitive;
use bigdecimal::ToPrimitive;
Expand All @@ -26,12 +20,20 @@ use serde::Serialize;
use serde_with::serde_as;
use serde_with::TimestampSeconds;
use serenity::model::id::GuildId;
use std::convert::TryFrom;
use std::num::TryFromIntError;
use std::path::PathBuf;
use std::time::SystemTime;
use tokio::fs;

use crate::api::auth::UserId;
use crate::api::Snowflake;
use crate::audio_utils;
use crate::db::models;
use crate::db::DbConn;
use crate::discord::management::check_guild_moderator;
use crate::discord::management::check_guild_user;
use crate::discord::management::get_guilds_for_user;
use crate::discord::management::PermissionError;
use crate::file_handling;
use crate::CacheHttp;

pub fn get_routes() -> Vec<Route> {
routes![
list_sounds,
Expand Down Expand Up @@ -114,7 +116,7 @@ struct Sound {
#[serde_as(as = "TimestampSeconds<String>")]
created_at: SystemTime,
volume_adjustment: Option<f32>,
soundfile: Option<Soundfile>,
sound_file: Option<Soundfile>,
}

#[serde_as]
Expand Down Expand Up @@ -144,7 +146,7 @@ impl TryFrom<(models::Sound, Option<models::Soundfile>)> for Sound {
category: s.category,
created_at: s.created_at,
volume_adjustment: s.volume_adjustment,
soundfile: f.map(|f| Soundfile {
sound_file: f.map(|f| Soundfile {
max_volume: f.max_volume,
mean_volume: f.mean_volume,
length: f.length,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,19 @@
automatically play sounds when a key combination on your computer is pressed.</p
>
<p
>The generated script contains a personal auth token for your account. Do not share it with others. It expires after a week, at which
point you will have to re-download the AutoHotkey script.</p
>The generated script contains a personal auth token for your account. Do not share it with others. You can regenerate it manually,
making all previously downloaded scripts invalid.</p
>
<p class="auth-token-row" [ngSwitch]="authToken() != null">
<ng-container *ngSwitchCase="false">
<span>No auth token generated.</span>
<button mat-button (click)="regenerateToken()"><mat-icon>autorenew</mat-icon> Generate token</button>
</ng-container>
<ng-container *ngSwitchCase="true">
<span>Auth token generated on {{ authToken().createdAt * 1000 | date : 'short' }}</span>
<button mat-button (click)="regenerateToken()"><mat-icon>autorenew</mat-icon> Regenerate token</button>
</ng-container>
</p>
<div class="table-wrapper mat-elevation-z8">
<mat-table [dataSource]="keybinds" cdkDropList [cdkDropListData]="keybinds" (cdkDropListDropped)="onDrop($event)">
<ng-container matColumnDef="dragDrop">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
.max-width {
max-width: 1500px;
margin: 0 auto;
padding: 0 16px;
width: 100%;
}

Expand All @@ -28,11 +29,13 @@ mat-toolbar {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
margin: 0 16px;
gap: 8px;
}

button {
margin-bottom: 8px;
}
.auth-token-row {
display: flex;
align-items: center;
gap: 8px;
}

.table-wrapper {
Expand Down
Loading

0 comments on commit deafc5a

Please sign in to comment.