diff --git a/.sqlx/query-50ccdaf65a2fe4a0596ed467890a1cd1722219fef41b80eaf5c7a9c20e2704a7.json b/.sqlx/query-50ccdaf65a2fe4a0596ed467890a1cd1722219fef41b80eaf5c7a9c20e2704a7.json new file mode 100644 index 0000000..e0a8c33 --- /dev/null +++ b/.sqlx/query-50ccdaf65a2fe4a0596ed467890a1cd1722219fef41b80eaf5c7a9c20e2704a7.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT user_name, user_id, session_id\n FROM application_access_tokens\n INNER JOIN application_sessions USING (session_id)\n INNER JOIN users USING (user_id)\n WHERE access_token_hash = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "user_name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "session_id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Bytea" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "50ccdaf65a2fe4a0596ed467890a1cd1722219fef41b80eaf5c7a9c20e2704a7" +} diff --git a/src/store.rs b/src/store.rs index 0a11630..0feed2b 100644 --- a/src/store.rs +++ b/src/store.rs @@ -9,6 +9,7 @@ use sqlx::{types::Uuid, Connection, PgPool, Postgres, Transaction}; use crate::web::login::{ LoginSession, LOGIN_SESSION_TOKEN_HASH_BYTES, LOGIN_SESSION_XSRF_SECRET_BYTES, }; +use crate::web::oauth_openid::application_session_access_token::ApplicationSession; /// Postgres-backed storage for IdCoop pub struct IdCoopStore { @@ -299,4 +300,31 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> { .map_err(|_| eyre!("cannot retrieve login session: has invalid XSRF token"))?, })) } + + pub async fn lookup_application_session( + &mut self, + access_token_hash: &[u8; 32], + ) -> eyre::Result> { + let row_opt = sqlx::query!( + " + SELECT user_name, user_id, session_id + FROM application_access_tokens + INNER JOIN application_sessions USING (session_id) + INNER JOIN users USING (user_id) + WHERE access_token_hash = $1 + ", + access_token_hash + ) + .fetch_optional(&mut **self.txn) + .await + .context("failed to lookup application session")?; + + let Some(row) = row_opt else { return Ok(None); }; + + Ok(Some(ApplicationSession { + application_session_id: row.session_id, + user_name: row.user_name, + user_id: row.user_id, + })) + } } diff --git a/src/web/oauth_openid.rs b/src/web/oauth_openid.rs index f8cab5d..2d12d2d 100644 --- a/src/web/oauth_openid.rs +++ b/src/web/oauth_openid.rs @@ -3,15 +3,31 @@ use std::sync::Arc; use axum::{http::StatusCode, response::IntoResponse, Extension, Json}; use josekit::jwk::Jwk; use serde::Serialize; +use sqlx::types::Uuid; use crate::config::{Configuration, SecretConfig}; +use self::application_session_access_token::ApplicationSession; + +pub mod application_session_access_token; pub mod authorisation; pub mod ext_codes; pub mod token; -pub async fn oidc_userinfo() -> impl IntoResponse { - (StatusCode::NOT_IMPLEMENTED, "NOT YET IMPLEMENTED") +#[derive(Serialize)] +struct UserInfoResponse { + sub: Uuid, + name: String, + preferred_username: String, + // TODO more... +} + +pub async fn oidc_userinfo(application_session: ApplicationSession) -> impl IntoResponse { + Json(UserInfoResponse { + sub: application_session.user_id, + name: application_session.user_name.clone(), + preferred_username: application_session.user_name, + }) } #[derive(Serialize)] diff --git a/src/web/oauth_openid/application_session_access_token.rs b/src/web/oauth_openid/application_session_access_token.rs new file mode 100644 index 0000000..52df2d8 --- /dev/null +++ b/src/web/oauth_openid/application_session_access_token.rs @@ -0,0 +1,67 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use axum::{ + extract::FromRequestParts, + headers::{authorization::Bearer, Authorization}, + http::{request::Parts, StatusCode}, + Extension, TypedHeader, +}; +use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine}; +use blake2::{Blake2s256, Digest}; +use sqlx::types::Uuid; +use tracing::error; + +use crate::store::IdCoopStore; + +pub struct ApplicationSession { + pub application_session_id: i32, + pub user_name: String, + pub user_id: Uuid, +} + +#[async_trait] +impl FromRequestParts for ApplicationSession +where + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let Ok(TypedHeader(Authorization(bearer))) = TypedHeader::>::from_request_parts(parts, state).await else { + return Err((StatusCode::UNAUTHORIZED, "No access token.")); + }; + let Ok(access_token) = BASE64_URL_SAFE_NO_PAD.decode(&bearer.token()) else { + return Err(( + StatusCode::UNAUTHORIZED, + "Invalid access token." + )); + }; + let access_token_hash: [u8; 32] = Blake2s256::digest(&access_token).into(); + + let db_store = Extension::>::from_request_parts(parts, state) + .await + .expect("no db store; this is a programming error"); + + match db_store + .txn(|mut txn| { + Box::pin(async move { txn.lookup_application_session(&access_token_hash).await }) + }) + .await + { + Ok(Some(session)) => Ok(session), + Ok(None) => { + return Err((StatusCode::UNAUTHORIZED, "Invalid application session.")); + } + Err(err) => { + error!("failed to check application session: {err:?}"); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + "A fault occurred when checking your application session. If the issue persists, please contact an administrator.", + )); + } + } + + // TODO do we want a middleware to renew the cookie? + } +}