Skip to content

Commit

Permalink
rework /auth into /token and read from db
Browse files Browse the repository at this point in the history
  • Loading branch information
jbellerb committed Mar 9, 2023
1 parent c7303c9 commit be90139
Show file tree
Hide file tree
Showing 12 changed files with 229 additions and 49 deletions.
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ edition = "2021"
anyhow = "1.0"
axum = { version = "0.6", features = ["macros"] }
base64ct = { version = "1.5", features = ["alloc"] }
ed25519-compact = "2.0"
pasetors = "0.6"
regex = "1.7"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
time = "0.3"
tokio = { version = "1.25", features = ["full"] }
tokio-postgres = "0.7"
tokio-postgres = { version = "0.7", features = ["with-time-0_3", "with-uuid-1"] }
tower = "0.4"
tower-cookies = "0.9"
tower-http = { version = "0.3", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1.3", features = ["v4", "fast-rng"] }
1 change: 1 addition & 0 deletions migrations/20230308182317_session.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE session;
10 changes: 10 additions & 0 deletions migrations/20230308182317_session.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE session (
id uuid NOT NULL PRIMARY KEY,
uid character varying(255) NOT NULL,
name character varying NOT NULL,
email character varying NOT NULL,
groups character varying[],
expire timestamp with time zone NOT NULL
);

CREATE INDEX session_expire_idx ON session (expire);
14 changes: 14 additions & 0 deletions scripts/gen_keys.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env sh

pack() {
while read -r line; do
printf '%s\\n' "$line"
done
echo "$line"
}

SECRET_KEY=$(openssl genpkey -algorithm ed25519)
PUBLIC_KEY=$(echo "$SECRET_KEY" | openssl pkey -pubout)

echo "POSER_AUTH_SECRET_KEY=\"$(echo "$SECRET_KEY" | pack)\""
echo "POSER_AUTH_PUBLIC_KEY=\"$(echo "$PUBLIC_KEY" | pack)\""
5 changes: 5 additions & 0 deletions scripts/migrate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env sh

DATABASE_URI=${POSER_AUTH_DATABASE_URI:-postgresql://poser@localhost/poser}

psql "$DATABASE_URI" < "$1"
21 changes: 21 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use std::path::PathBuf;
use std::time::Duration;

use base64ct::{Base64, Encoding};
use ed25519_compact::SecretKey;
use pasetors::keys::AsymmetricSecretKey;
use pasetors::{keys::SymmetricKey, version4::V4};
use regex::Regex;
use thiserror::Error;
Expand All @@ -18,12 +20,16 @@ pub enum ConfigError {
InvalidEnvVar,
#[error("invalid socket address")]
InvalidAddr,
#[error("invalid secret key")]
InvalidSecretKey,
#[error("invalid base64")]
InvalidBase64,
#[error("invalid symmetric key")]
InvalidSymmetricKey,
#[error("invalid duration")]
InvalidDuration,
#[error("missing secret key")]
MissingSecretKey,
#[error("missing cookie secret")]
MissingCookieSecret,
#[error("missing Google client id")]
Expand All @@ -39,6 +45,9 @@ pub const ENV_LISTEN_ADDR: &str = "POSER_AUTH_LISTEN_ADDR";
pub const ENV_DATABASE_URI: &str = "POSER_AUTH_DATABASE_URI";
pub const ENV_SHUTDOWN_GRACE_PERIOD: &str = "POSER_AUTH_SHUTDOWN_GRACE_PERIOD";

/// An OpenSSL-compatible, PEM encoded ed25519 private key.
pub const ENV_SECRET_KEY: &str = "POSER_AUTH_SECRET_KEY";

pub const ENV_COOKIE_NAME: &str = "POSER_AUTH_COOKIE_NAME";
pub const ENV_COOKIE_SECRET: &str = "POSER_AUTH_COOKIE_SECRET";

Expand All @@ -64,6 +73,7 @@ pub const DEFAULT_GOOGLE_SERVICE_ACCOUNT: &str = "/data/service_account.json";
pub struct Config {
pub addr: SocketAddr,
pub database: String,
pub key: AsymmetricSecretKey<V4>,
pub cookie: CookieConfig,
pub google: GoogleConfig,
pub grace_period: Duration,
Expand Down Expand Up @@ -97,6 +107,16 @@ impl Config {

let database = get_env_default(ENV_DATABASE_URI, DEFAULT_DATABASE_URI)?;

let key_raw = get_env(ENV_SECRET_KEY)?.ok_or_else(|| {
error!("expected private key");
ConfigError::MissingSecretKey
})?;
let key = SecretKey::from_pem(&key_raw).map_err(|e| {
error!("failed to parse private key: {}", e);
ConfigError::InvalidSecretKey
})?;
let key = AsymmetricSecretKey::<V4>::from(&*key).unwrap();

let cookie = {
let name = get_env_default(ENV_COOKIE_NAME, DEFAULT_COOKIE_NAME)?;

Expand Down Expand Up @@ -151,6 +171,7 @@ impl Config {
Ok(Config {
addr,
database,
key,
cookie,
google,
grace_period,
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub mod config;
pub mod error;
pub mod oidc;
pub mod routes;
pub mod token;

use std::env::var;
use std::sync::Arc;
Expand Down
44 changes: 0 additions & 44 deletions src/routes/auth.rs

This file was deleted.

8 changes: 4 additions & 4 deletions src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
//! HTTP routes for interacting with poser.
pub mod auth;
pub mod callback;
pub mod login;
pub mod token;

use crate::ServerState;
use auth::auth_handler;
use callback::callback_handler;
use login::login_handler;
use token::token_handler;

use axum::{
http::StatusCode,
routing::{get, Router},
routing::{get, post, Router},
};

/// The complete router for this application.
pub fn routes() -> Router<ServerState> {
Router::new()
.route("/auth", get(auth_handler))
.route("/token", post(token_handler))
.route("/callback", get(callback_handler))
.route("/login", get(login_handler))
.route("/ping", get(ping_handler))
Expand Down
104 changes: 104 additions & 0 deletions src/routes/token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! A route for requesting tokens from a user session.
use std::collections::HashMap;

use crate::token::{self, UserToken};
use crate::ServerState;

use axum::{
extract::{Form, State},
http::StatusCode,
response::{IntoResponse, Json, Response},
};
use pasetors::{keys::AsymmetricSecretKey, version4::V4};
use serde_json::json;
use thiserror::Error;
use time::OffsetDateTime;
use tokio_postgres::Client;
use tracing::error;
use uuid::Uuid;

/// Errors returned by a the handler.
#[derive(Error, Clone, Debug)]
pub enum TokenError {
#[error("invalid session token")]
InvalidSessionToken,
#[error("invalid session")]
InvalidSession,
#[error("missing session token")]
MissingSessionToken,
#[error("error generating id token")]
PasetoError(#[from] token::TokenError),
}

/// A handler to generate a short-lived id token from a user's session token.
///
/// Given a session token (stored in the auth cookie), this route either
/// returns 200 OK with a short-lived Paseto of the user or 400 Bad Request
/// with an error message and possibly the configured login URL for
/// redirecting the user.
#[axum::debug_handler(state = ServerState)]
pub async fn token_handler(
State(state): State<ServerState>,
Form(params): Form<HashMap<String, String>>,
) -> Result<Response, TokenError> {
let session_token = params.get("code").ok_or_else(|| {
error!("missing session token parameter");
TokenError::MissingSessionToken
})?;

let session_id = Uuid::try_parse(session_token).map_err(|_| {
error!("failed to parse session token as uuid");
TokenError::InvalidSessionToken
})?;

let token = build_token(&session_id, &state.db, &state.config.key).await?;

Ok(Json(json!({ "expires_in": 3600, "id_token": token })).into_response())
}

async fn build_token(
session_id: &Uuid,
db: &Client,
key: &AsymmetricSecretKey<V4>,
) -> Result<String, TokenError> {
let session = db
.query_one("SELECT * from session WHERE id = $1::UUID", &[&session_id])
.await
.map_err(|e| {
error!("database error: {}", e);
TokenError::InvalidSession
})?;

let expiration: OffsetDateTime = session.get("expire");
if expiration < OffsetDateTime::now_utc() {
error!("session is expired");
return Err(TokenError::InvalidSession);
}

let token = UserToken {
id: session.get("uid"),
name: session.get("name"),
email: session.get("email"),
groups: session.get("groups"),
};

token.sign(key).map_err(|e| {
error!("error generating token: {}", e);
TokenError::PasetoError(e)
})
}

impl IntoResponse for TokenError {
fn into_response(self) -> Response {
let response = match self {
TokenError::InvalidSessionToken | TokenError::MissingSessionToken => {
json!({ "error": "invalid request" })
}
TokenError::InvalidSession => json!({ "error": "bad session" }),
TokenError::PasetoError(_) => json!({ "error": "internal error" }),
};

(StatusCode::BAD_REQUEST, Json(response)).into_response()
}
}
Loading

0 comments on commit be90139

Please sign in to comment.