Improve code documentation

Signed-off-by: Olivier 'reivilibre <olivier@librepush.net>
This commit is contained in:
Olivier 'reivilibre' 2024-06-26 19:31:22 +01:00
parent 9bbae5411b
commit 8121a8996e
12 changed files with 282 additions and 9 deletions

View File

@ -1,3 +1,11 @@
//! # idCoop Configuration
//!
//! [`confique`] is used to allow for a layered configuration.
//!
//! We have two files: a configuration file and a 'secrets' file.
//! The secrets file only contains secrets and is suitable for keeping encrypted and
//! separate from the main configuration in deployment tools.
use std::{
collections::{BTreeMap, BTreeSet},
net::SocketAddr,
@ -12,24 +20,34 @@ use governor::Quota;
use josekit::jwk::alg::rsa::RsaKeyPair;
use serde::{de::Error, Deserialize};
/// Root of the main configuration file.
///
/// This is split into several sections.
#[derive(Config)]
pub struct Configuration {
/// Section of configuration controlling how the web interface listens.
#[config(nested)]
pub listen: ListenConfiguration,
/// Section of configuration controlling how we connect to Postgres.
#[config(nested)]
pub postgres: PostgresConfiguration,
/// Section of configuration for setting up OpenID Connect clients.
#[config(nested)]
pub oidc: OidcConfiguration,
/// Section of configuration for configuring how passwords are hashed.
#[config(nested)]
pub password_hashing: PasswordHashingConfig,
/// Section of configuration for configuring how requests are rate limited
/// to prevent overload of the system or exceeding system requirements.
#[config(nested)]
pub ratelimits: RatelimitsConfig,
}
/// Configuration of values and clients for OpenID Connect.
#[derive(Config)]
pub struct OidcConfiguration {
/// Our identity as an OAuth 2.1 / OIDC issuer.
@ -37,6 +55,7 @@ pub struct OidcConfiguration {
/// Should really be unique.
pub issuer: String,
/// Path to an RSA keypair in PEM format.
pub rsa_keypair: PathBuf,
/// Map from client_id to OpenID Connect Relying Party (client) configuration.
@ -44,26 +63,39 @@ pub struct OidcConfiguration {
pub clients: BTreeMap<String, OidcClientConfiguration>,
}
/// Configuration of how the HTTP interface should listen.
#[derive(Config)]
pub struct ListenConfiguration {
/// What host and port to bind to. e.g. `127.0.0.1:8177`
#[config(env = "IC_LISTEN_BIND")]
pub bind: Option<SocketAddr>,
/// The public base URI, e.g. `https://idcoop.example.org`.
/// This is how users will access idCoop.
/// Please use a HTTPS URI in real-world circumstances.
pub public_base_uri: String,
/// How to resolve client IP addresses.
///
/// See [`SecureClientIpSource`] for the options.
/// The default uses the IP address of the TCP client, which assumes a reverse proxy is not in use.
/// A popular alternative would be to use the `X-Forwarded-For` header and configure your reverse proxy
/// (e.g. nginx) to set that.
#[config(default = "ConnectInfo")]
pub client_ip_source: SecureClientIpSource,
}
/// Configuration for connecting to Postgres
#[derive(Config)]
pub struct PostgresConfiguration {
/// Postgres connection string in the form:
/// `postgres://user:pass@host:port/db_name?application-name=idcoop`
/// See https://docs.rs/sqlx-postgres/0.7.2/sqlx_postgres/struct.PgConnectOptions.html
/// See <https://docs.rs/sqlx-postgres/0.7.2/sqlx_postgres/struct.PgConnectOptions.html>
#[config(env = "IC_POSTGRES")]
pub connect: String,
}
/// Configuration for one individual OpenID Connect client (application / relying party).
#[derive(Deserialize)]
pub struct OidcClientConfiguration {
/// Whether to skip the consent screen. Usually desirable for SSO where you control the service and the identity server.
@ -92,6 +124,7 @@ pub struct OidcClientConfiguration {
pub secret: Option<String>,
}
/// Content of the secret configuration file; separate from the main configuration file.
#[derive(Config)]
pub struct SeparateSecretConfiguration {
/// Client secrets for the OIDC clients.
@ -99,11 +132,16 @@ pub struct SeparateSecretConfiguration {
pub oidc_secrets: BTreeMap<String, String>,
}
/// Loaded secrets
pub struct SecretConfig {
/// The RSA key pair used for OpenID Connect.
pub rsa_key_pair: RsaKeyPair,
}
impl SecretConfig {
/// Attempt to load the secrets.
///
/// Fails if we can't read the RSA key for example.
pub async fn try_new(config: &Configuration) -> eyre::Result<Self> {
let rsa_keypair_bytes = tokio::fs::read(&config.oidc.rsa_keypair)
.await
@ -132,13 +170,22 @@ pub struct PasswordHashingConfig {
pub parallelism: u32,
}
/// Configuration for rate-limits.
#[derive(Config)]
pub struct RatelimitsConfig {
/// How many login attempts to allow from one source IP address.
#[config(default = "10 per hour, 5 burst")]
pub login: RatelimiterConfig,
}
/// Configuration for one rate limit.
///
/// Is read from the configuration file in one of these formats:
/// - "5 Hz, 20 burst"
/// - "5 per second, 10 burst"
/// - "10 per hour, 5 burst"
pub struct RatelimiterConfig {
/// The inner [`Quota`], which this struct is just a wrapper for.
pub quota: Quota,
}

View File

@ -1,3 +1,10 @@
//! # the idCoop crate
//!
//! idCoop is an application but this crate is exposed to allow for easier integration testing.
//! (This is just personal style: the entrypoint is in `src/bin` and is only a thin wrapper around the `idcoop` crate.)
#![deny(missing_docs)]
pub mod config;
pub mod passwords;
pub mod store;

View File

@ -1,3 +1,7 @@
//! Password handling utilities for idCoop.
//!
//! This file is particularly security-sensitive.
use argon2::{
password_hash::SaltString, Algorithm, Argon2, Params, PasswordHash, PasswordHasher,
PasswordVerifier, Version,
@ -7,6 +11,11 @@ use rand::rngs::OsRng;
use crate::config::PasswordHashingConfig;
/// Create a password hash from the given password string and configuration.
///
/// The returned result should only be STORED AS-IS and then used with [`check_hash`] later.
///
/// It is not valid to compare results of this function directly.
pub fn create_password_hash(
password: &str,
config: &PasswordHashingConfig,
@ -24,6 +33,13 @@ pub fn create_password_hash(
.map(|h| h.to_string())
}
/// Check a password hash, which was created with [`create_password_hash`] beforehand,
/// against a user-entered password string.
///
/// Under no circumstances should the user be able to control the password hash.
///
/// Returns true if the password is valid.
/// Returns false if the wrong password was supplied.
pub fn check_hash(password: &str, hash: &str) -> eyre::Result<bool> {
let argon2 = Argon2::new(
Algorithm::Argon2id,

View File

@ -1,3 +1,7 @@
//! # idCoop database access
//!
//! This file contains PostgreSQL queries.
use chrono::DateTime;
use chrono::NaiveDateTime;
use chrono::Utc;
@ -12,11 +16,20 @@ use crate::web::login::{
use crate::web::oauth_openid::application_session_access_token::ApplicationSession;
/// Postgres-backed storage for IdCoop
/// A connection pool is in use.
pub struct IdCoopStore {
db_pool: PgPool,
}
impl IdCoopStore {
/// Connect to a Postgres database using the given Postgres connection URI.
///
/// Once connected, apply database migrations to update the database.
///
/// The simplest example of a connection URI is `postgres:`,
/// which will connect according to libpq environment variables and defaults.
///
/// A description of the exact behaviour should be sought from the SQLx documentation: [`sqlx::postgres::PgConnectOptions`].
pub async fn connect(conn_str: &str) -> eyre::Result<Self> {
// TODO(sqlite) We might support SQLite as well, so we might want to use an 'AnyPool' after
// installing the requisite drivers here
@ -56,32 +69,53 @@ impl IdCoopStore {
}
}
/// Representation of a user
#[derive(Debug)]
pub struct User {
/// The unique ID for the user.
/// Should never change.
pub user_id: Uuid,
/// The unique system name for the user.
/// It is ill-advised to change this but real-world constraints may require it.
pub user_name: String,
/// The time when this user was created, in UTC.
pub created_at_utc: NaiveDateTime,
/// The password hash of the user. See [`crate::passwords`].
pub password_hash: Option<String>,
/// Whether the user is locked and is therefore not allowed to log in.
pub locked: bool,
}
/// Representation of the action of creating a user.
pub struct CreateUser {
/// The system name for the user.
pub user_login_name: String,
/// The password hash of the user. See [`crate::passwords`].
pub password_hash: Option<String>,
/// Whether the user is locked and is therefore not allowed to log in.
pub locked: bool,
}
/// Basic information about a user
pub struct UserInfo {
/// The unique system name for the user.
/// It is ill-advised to change this but real-world constraints may require it.
pub user_name: String,
/// The unique ID for the user.
/// Should never change.
pub user_id: Uuid,
/// Whether the user is locked and is therefore not allowed to log in.
pub locked: bool,
}
/// A wrapper around a database transaction with some database methods on it.
pub struct IdCoopStoreTxn<'a, 'txn> {
txn: &'a mut Transaction<'txn, Postgres>,
}
impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
/// Given the hash of an access token and a hash of a refresh token,
/// invalidates both the access and refresh tokens.
pub async fn invalidate_access_token_by_hash(
&mut self,
access_token_hash: &[u8],
@ -147,6 +181,9 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
Ok(Some(row.session_id))
}
/// Given its hash, issue a new access token.
///
/// The access token is associated with the given session ID and expires at the given time.
pub async fn issue_access_token(
&mut self,
access_token_hash: &[u8],
@ -167,6 +204,9 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
Ok(())
}
/// Given its hash, issue a new refresh token.
///
/// The refresh token is associated with the given session ID and expires at the given time.
pub async fn issue_refresh_token(
&mut self,
refresh_token_hash: &[u8],
@ -199,6 +239,7 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
Ok(r.user_id)
}
/// Given a user's system name, return their user record if they exist.
pub async fn lookup_user_by_name(&mut self, name: String) -> eyre::Result<Option<User>> {
sqlx::query_as!(
User,
@ -209,6 +250,7 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
.await.context("failed to lookup user from DB")
}
/// Set a given user's password.
pub async fn change_user_password(
&mut self,
user_id: Uuid,
@ -225,6 +267,7 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
Ok(())
}
/// Set the locked status of a user.
pub async fn set_user_locked(&mut self, user_id: Uuid, locked: bool) -> eyre::Result<()> {
sqlx::query!(
"UPDATE users SET locked = $1 WHERE user_id = $2",
@ -237,6 +280,10 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
Ok(())
}
/// Delete a user.
///
/// Note that this can sometimes be undesirable, as downstream clients won't realise
/// the system name has been freed.
pub async fn delete_user(&mut self, user_id: Uuid) -> eyre::Result<()> {
sqlx::query!("DELETE FROM users WHERE user_id = $1", user_id)
.execute(&mut **self.txn)
@ -245,6 +292,7 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
Ok(())
}
/// List all users, in order of their user name, returning brief information about them.
pub async fn list_user_info(&mut self) -> eyre::Result<Vec<UserInfo>> {
sqlx::query_as!(
UserInfo,
@ -255,6 +303,10 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
.context("failed to list users")
}
/// Create a login (~'web UI') session for the given user, with the given hash of the login session's token.
///
/// The XSRF secret is generated specifically for this session
/// and is used to prevent cross-site request forgery (XSRF) attacks.
pub async fn create_login_session(
&mut self,
login_session_token_hash: &[u8; LOGIN_SESSION_TOKEN_HASH_BYTES],
@ -272,6 +324,7 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
Ok(())
}
/// Given the login session's token hash, looks up its corresponding login session.
pub async fn lookup_login_session(
&mut self,
login_session_token_hash: &[u8; LOGIN_SESSION_TOKEN_HASH_BYTES],
@ -301,6 +354,7 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
}))
}
/// Given an access token's hash, looks up the corresponding application session.
pub async fn lookup_application_session(
&mut self,
access_token_hash: &[u8; 32],

View File

@ -1,3 +1,13 @@
//! # idCoop Web Interface
//!
//! This large module exposes the web/HTTP interface of idCoop.
//!
//! The web interface is used for:
//!
//! - users for logging in and managing their own account
//! - administrators for managing idCoop and their users
//! - applications making OpenID Connect requests to check users
use std::{
net::{IpAddr, SocketAddr},
sync::{
@ -170,13 +180,17 @@ impl IntoResponse for InternalEyreError {
}
}
/// A convenient `Result` type for web requests.
pub type WebResult<T> = Result<T, InternalEyreError>;
/// Perform housekeeping on the ratelimiters every 60 seconds.
const HOUSEKEEPING_EVERY: u32 = 60 >> 2;
type IpRateLimiter = RateLimiter<IpAddr, DashMapStateStore<IpAddr>, QuantaClock>;
/// Constructed rate limiters
pub struct Ratelimiters {
/// The login ratelimiter for `/login`
pub login: IpRateLimiter,
/// Time of last housekeeping operation, since Unix epoch, but shifted right by 2 places
@ -184,6 +198,7 @@ pub struct Ratelimiters {
}
impl Ratelimiters {
/// Create a new bunch of rate limiters from configuration
pub fn new_from_config(config: &RatelimitsConfig) -> Self {
Self {
login: Self::governor_from_config(&config.login),

View File

@ -1,3 +1,5 @@
//! `/login` and related utilities
use std::{
collections::BTreeMap,
net::IpAddr,
@ -46,6 +48,9 @@ pub struct PasswordHashInflightLimiter {
}
impl PasswordHashInflightLimiter {
/// Create a new password hashing in-flight limiter.
///
/// - `limit` is the maximum number of allowed concurrent password hashing operations.
pub fn new(limit: usize) -> Self {
Self {
overall: Semaphore::new(limit),
@ -53,6 +58,14 @@ impl PasswordHashInflightLimiter {
}
}
/// Call a function `f` that is subject to this limit.
/// The source IP address `src_ip` is the IP address of the client on whose behalf this computation
/// is being performed.
///
/// A single source IP address will not be allowed to hog more than one slot.
///
/// Some level of fairness is intended here: a requester can't add more requests to the queue
/// whilst one of its requests is being handled, so other requesters get a fair chance in the queue.
pub async fn do_limited<R>(
&self,
src_ip: IpAddr,
@ -115,10 +128,15 @@ pub const LOGIN_SESSION_TOKEN_HASH_BYTES: usize = 32;
/// e.g. perhaps the persona should be a hash of the user's UUID?
pub const LOGIN_SESSION_XSRF_SECRET_BYTES: usize = 32;
/// Represents a login session, which is effectively just a 'web UI' session for a user.
pub struct LoginSession {
/// The system name of the user who is logged in to the idCoop web UI
pub user_name: String,
/// The UUID of the user who is logged in to the idCoop web UI
pub user_id: Uuid,
/// The ID of this login session
pub login_session_id: i32,
/// The XSRF (cross-site request forgery) prevention secret of this login session
pub xsrf_secret: [u8; LOGIN_SESSION_XSRF_SECRET_BYTES],
}
@ -230,11 +248,22 @@ where
}
}
/// Query string parameters for `GET /login`
#[derive(Deserialize)]
pub struct LoginQuery {
/// Which page to follow after the login has completed?
/// This is the path and query component of a URI.
///
/// Security-wise, this must be 'local' to idCoop;
/// i.e. we don't allow redirecting users to other places after they log in.
then: Option<String>,
}
/// `GET /login?then=...`
///
/// If logged in, redirects to `then` (if safe to do so) immediately.
///
/// If not logged in, shows a login form.
pub async fn get_login(
current_session: Option<LoginSession>,
Query(query): Query<LoginQuery>,
@ -249,13 +278,21 @@ pub async fn get_login(
}
}
/// Body parameters for `POST /login`
#[derive(Deserialize)]
pub struct PostLoginForm {
/// User-supplied username
username: String,
/// User-supplied password
password: String,
/// Hidden XSRF token, used to check this request isn't being made from another site
/// (i.e. protects against cross-site request forgery)
xsrf: String,
}
/// Perform a password hash operation, even though it's not needed.
///
/// This aims to prevent side-channel attacks such as timing attacks, to discern the existence of a user.
fn dummy_password_hash(
password: String,
password_hash_config: &PasswordHashingConfig,
@ -266,10 +303,29 @@ fn dummy_password_hash(
Ok(render_login_retry_form())
}
/// Render the form to retry a login. Currently not implemented.
fn render_login_retry_form() -> Response {
(StatusCode::UNAUTHORIZED, "Wrong username or password!").into_response() // TODO(ui): this should re-render the login form for another go
}
/// `POST /login?then=...`
///
/// Performs a login attempt.
///
/// The login attempt is rate-limited, to prevent password brute-force attacks.
///
/// The form is XSRF protected using [`crate::web::sessionless_xsrf`]
/// (given we don't have a working login session to use sessionful XSRF protection).
///
/// We prevent requesters from trying to discern if given usernames exist or not;
/// whether or not a user exists, a password hash operation will be performed which takes
/// a certain amount of time.
///
/// The response also does not point out that a username doesn't exist.
///
/// If the login is successful, a new login session is created, which is stored in
/// browser cookies and the user is redirected to what they were trying to access
/// that needed the login in the first place.
#[allow(clippy::too_many_arguments)]
pub async fn post_login(
Query(query): Query<LoginQuery>,
@ -377,6 +433,11 @@ pub async fn post_login(
.into_response())
}
/// Make a redirect for once the user has logged in.
///
/// We verify that the redirect URI is just a path and query component,
/// to ensure we don't redirect cross-domain or any silly nonsense like that.
/// This is because cross-domain redirects could be a security hazard.
fn make_post_login_redirect(then: Option<String>) -> Response {
match then {
Some(then) => match PathAndQuery::try_from(then) {

View File

@ -1,3 +1,5 @@
//! OpenID Connect / OAuth 2.1 support
use std::sync::Arc;
use axum::{http::StatusCode, response::IntoResponse, Extension, Json};
@ -22,6 +24,7 @@ struct UserInfoResponse {
// TODO more...
}
/// `GET /oidc/userinfo`
pub async fn oidc_userinfo(application_session: ApplicationSession) -> impl IntoResponse {
Json(UserInfoResponse {
sub: application_session.user_id,
@ -35,6 +38,7 @@ struct JsonWebKeySet {
keys: Vec<Jwk>,
}
/// `GET /oidc/jwks`
pub async fn oidc_jwks(Extension(secrets): Extension<Arc<SecretConfig>>) -> impl IntoResponse {
let mut jwk = secrets.rsa_key_pair.to_jwk_public_key();
jwk.set_key_use("sig");
@ -46,11 +50,12 @@ pub async fn oidc_jwks(Extension(secrets): Extension<Arc<SecretConfig>>) -> impl
(StatusCode::OK, Json(key_set))
}
/// `GET /.well-known/webfinger`
pub async fn oidc_discovery_webfinger() -> impl IntoResponse {
(StatusCode::NOT_IMPLEMENTED, "NOT YET IMPLEMENTED")
}
/// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
/// <https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata>
#[derive(Serialize)]
struct OidcDiscoveryConfiguration {
/// REQUIRED. URL using the https scheme with no query or fragment component that the OP asserts as its Issuer Identifier.
@ -112,6 +117,7 @@ struct OidcDiscoveryConfiguration {
//claims_supported: Vec<String>,
}
/// `GET /.well-known/openid-configuration`
pub async fn oidc_discovery_configuration(
Extension(config): Extension<Arc<Configuration>>,
) -> impl IntoResponse {

View File

@ -1,3 +1,5 @@
//! Middleware for extracting application sessions (access tokens) from requests.
use std::sync::Arc;
use async_trait::async_trait;
@ -14,9 +16,13 @@ use tracing::error;
use crate::store::IdCoopStore;
/// Session between a user and an OpenID Connect client (application / relying party).
pub struct ApplicationSession {
/// The ID of this session
pub application_session_id: i32,
/// The system user name of the user
pub user_name: String,
/// The user ID of the user
pub user_id: Uuid,
}
@ -61,7 +67,5 @@ where
));
}
}
// TODO do we want a middleware to renew the cookie?
}
}

View File

@ -1,3 +1,5 @@
//! `/oidc/auth`
use std::sync::Arc;
use axum::{
@ -24,6 +26,7 @@ use crate::{
use super::ext_codes::VolatileCodeStore;
/// Query string parameters for the OIDC authorisation request.
#[derive(Deserialize, Debug)]
pub struct AuthorisationQuery {
/// REQUIRED. OpenID Connect requests MUST contain the openid scope value.
@ -57,9 +60,10 @@ pub struct AuthorisationQuery {
/// Sadly this is not specified in the spec, but 384 seems like a real-world practical value.
/// (Some clients have been known to generate nonces at least 300 bytes long!)
/// https://github.com/openid-certification/oidctest/issues/134
/// <https://github.com/openid-certification/oidctest/issues/134>
pub const MAX_NONCE_LENGTH: usize = 384;
/// `GET /oidc/auth`
pub async fn oidc_authorisation(
query: Result<Query<AuthorisationQuery>, QueryRejection>,
login_session: Option<LoginSession>,
@ -121,12 +125,14 @@ pub async fn oidc_authorisation(
.await
}
/// Body parameters for OIDC authorisation consent.
#[derive(Deserialize)]
pub struct PostConsentForm {
action: String,
xsrf: String,
}
/// `POST /oidc/auth`
pub async fn post_oidc_authorisation_consent(
Query(query): Query<AuthorisationQuery>,
login_session: Option<LoginSession>,
@ -406,6 +412,7 @@ fn extend_uri_with_params(uri: Uri, params: impl Serialize) -> eyre::Result<Uri>
/// OpenID Connect Core: https://openid.net/specs/openid-connect-core-1_0.html#AuthError
/// (Not all errors from OpenID Connect Core were included.)
#[derive(Copy, Clone, Serialize, Deserialize)]
#[allow(missing_docs)]
pub enum AuthorisationRedirectableError {
#[serde(rename = "invalid_request")]
InvalidRequest,

View File

@ -1,3 +1,5 @@
//! Authorisation codes
use std::{
collections::{BTreeSet, HashMap},
fmt::Display,
@ -16,9 +18,13 @@ use tokio::sync::Notify;
#[derive(Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
pub struct AuthCode([u8; 24]);
/// Access token
pub type AccessToken = [u8; 32];
/// Hash of an access token
pub type AccessTokenHash = [u8; 32];
/// Refresh token
pub type RefreshToken = [u8; 32];
/// Hash of a refresh token
pub type RefreshTokenHash = [u8; 32];
impl Display for AuthCode {
@ -47,26 +53,44 @@ impl FromStr for AuthCode {
}
impl AuthCode {
/// Generate a new authorisation code using the thread's RNG
pub fn generate_new_random() -> Self {
Self(rand::thread_rng().gen::<[u8; 24]>())
}
}
/// Binding between an authorisation code (ready to be redeemed)
/// and both the user that authenticated to produce it
/// as well as the OpenID Connect client that it is for.
pub struct AuthCodeBinding {
/// ID of the OpenID Connect client
pub client_id: String,
// TODO(memory): would be nice to intern this, since it is likely to be repeated a lot...
/// Redirect URI that was used to send this authorisation code back to the OpenID Connect
/// client. The client must present the same Redirect URI when redeeming the code.
/// TODO(memory): would be nice to intern this, since it is likely to be repeated a lot...
pub redirect_uri: String,
/// A 'number used once', as was specified in the authorisation request
/// Technically optional in the protocol but it's good practice to use one!
pub nonce: Option<String>,
/// The method for using the code challenge
pub code_challenge_method: String,
/// The code challenge
pub code_challenge: String,
/// The UUID of the user that authenticated to produce this authorisation code
pub user_id: Uuid,
/// The login session ID of the user that authenticated.
/// The authorisation code will not be considered valid when the login session is no longer valid.
pub user_login_session_id: i32,
}
/// The representation of an auth code that was redeemed by a client.
/// The auth code is not allowed to be doubly redeemed, so by remembering this,
/// we can revoke the tokens.
pub struct RedeemedAuthCode {
/// The access token hash of whoever redeemed the auth code.
/// Is used to invalidate the access token if an auth code is double-redeemed.
access_token_hash: AccessTokenHash,
/// The refresh token hash of the redeemer.
refresh_token_hash: RefreshTokenHash,
}
@ -140,6 +164,8 @@ impl VolatileCodeStoreInner {
}
}
/// Stores authorisation codes, but is volatile (not persisted to disk).
/// This is OK because they should be used practically immediately anyway.
#[derive(Clone)]
pub struct VolatileCodeStore {
poke: Arc<Notify>,
@ -194,6 +220,9 @@ impl VolatileCodeStore {
}
}
/// Redeem an authorisation code.
///
/// See [`CodeRedemption`] for a description of the possible outcomes here.
pub fn redeem(
&self,
auth_code: &AuthCode,
@ -204,6 +233,7 @@ impl VolatileCodeStore {
inner.redeem(auth_code, access_token_hash, refresh_token_hash)
}
/// Add a new redeemable authorisation code.
pub fn add_redeemable(&self, auth_code: AuthCode, binding: AuthCodeBinding, expires_at: u64) {
let mut inner = self.inner.lock().unwrap();
inner.add_redeemable(auth_code, binding, expires_at);
@ -213,15 +243,24 @@ impl VolatileCodeStore {
}
}
/// Possible outcomes of an attempt to redeem an authorisation code.
pub enum CodeRedemption {
/// That auth code was not active
Invalid,
/// That auth code is valid and has been redeemed.
Valid { binding: AuthCodeBinding },
Valid {
/// The binding, which describes what this authorisation code is for.
binding: AuthCodeBinding,
},
/// That auth code had already been redeemed: please invalidate the given access token and reject this redemption.
Conflicted {
// TODO(refresh) what if the token was refreshed since?
/// The hash of the access token that already redeemed this.
/// The access token should be invalidated.
access_token_to_invalidate: AccessTokenHash,
/// The hash of the refresh token that already redeemed this.
/// The refresh token should be invalidated.
///
/// TODO(refresh) what if the token was refreshed since?
refresh_token_to_invalidate: RefreshTokenHash,
},
}

View File

@ -1,3 +1,5 @@
//! `/oidc/token`
use std::{
str::FromStr,
sync::Arc,
@ -35,6 +37,7 @@ use super::ext_codes::{
VolatileCodeStore,
};
/// Body parameters for `POST /token`
#[derive(Deserialize)]
pub struct TokenFormParams {
grant_type: String,
@ -45,7 +48,11 @@ pub struct TokenFormParams {
// client_id: Option<String>,
}
// TODO auth_header can be one alternative
/// `POST /oidc/token`
///
/// OpenID Connect clients call this to exchange an authorisation code they received for an access token.
///
/// TODO auth_header can be one alternative auth method
pub async fn oidc_token(
basic_auth: Option<TypedHeader<Authorization<Basic>>>,
Extension(config): Extension<Arc<Configuration>>,
@ -399,6 +406,10 @@ fn make_id_token(id_token: IdToken, secrets: &SecretConfig) -> eyre::Result<Stri
.context("failed to sign ID Token")
}
/// Given a code challenge method and the code challenge verifier, compute
/// the 'code challenge'.
///
/// TODO(doc): provide spec reference for this
pub fn compute_code_challenge(method: &str, verifier: &str) -> Option<String> {
match method {
"plain" => Some(verifier.to_owned()),
@ -413,6 +424,11 @@ pub fn compute_code_challenge(method: &str, verifier: &str) -> Option<String> {
}
}
/// OpenID Connect ID Token
///
/// This is the payload of a JWT that is passed to the client/application.
///
/// TODO(doc): provide spec reference for this.
#[derive(Serialize)]
pub struct IdToken {
/// REQUIRED. Issuer Identifier for the Issuer of the response.

View File

@ -12,6 +12,7 @@ use subtle::ConstantTimeEq;
use time::Duration;
use tower_cookies::{Cookie, Cookies};
/// Name of the cookie for the sessionless cross-site request forgery prevention cookie.
pub const COOKIE_NAME: &str = "__Host-SessionlessXsrf";
/// Gets the Sessionless XSRF token to put into a form request