diff --git a/lib/komainu/src/authorize.rs b/lib/komainu/src/authorize.rs index e69de29bb..1a51a2749 100644 --- a/lib/komainu/src/authorize.rs +++ b/lib/komainu/src/authorize.rs @@ -0,0 +1,129 @@ +use crate::{ + error::{Error, Result}, + params::ParamStorage, + Client, ClientExtractor, OptionExt, +}; +use std::{collections::HashSet, future::Future}; + +pub trait Issuer { + type UserId; + + fn issue_code( + &self, + user_id: Self::UserId, + client_id: &str, + scopes: &[&str], + ) -> impl Future> + Send; +} + +pub struct AuthorizerExtractor { + issuer: I, + client_extractor: CE, +} + +impl AuthorizerExtractor +where + CE: ClientExtractor, +{ + pub fn new(issuer: I, client_extractor: CE) -> Self { + Self { + issuer, + client_extractor, + } + } + + pub async fn extract<'a>(&'a self, req: &'a http::Request<()>) -> Result> { + let query: ParamStorage<&str, &str> = + serde_urlencoded::from_str(req.uri().query().or_missing_param()?) + .map_err(Error::query)?; + + // TODO: Load client and verify the parameters (client ID, client secret, redirect URI, scopes, etc.) check out + // Error out if that's not the case + // + // Check the grant_type, let the client access it _somehow_ + // + // Give the user some kind of "state" parameter, preferably typed, so they can store the authenticated user, and their + // consent answer. + + let client_id = query.get("client_id").or_missing_param()?; + let response_type = query.get("response_type").or_missing_param()?; + if *response_type != "code" { + debug!(?client_id, "response_type not set to \"code\""); + return Err(Error::Unauthorized); + } + + let scope = query.get("scope").or_missing_param()?; + let redirect_uri = query.get("redirect_uri").or_missing_param()?; + let state = query.get("state").map(|state| &**state); + + let client = self.client_extractor.extract(client_id, None).await?; + if client.redirect_uri != *redirect_uri { + debug!(?client_id, "redirect uri doesn't match"); + return Err(Error::Unauthorized); + } + + let request_scopes = scope.split_whitespace().collect::>(); + let client_scopes = client + .scopes + .iter() + .map(|scope| &**scope) + .collect::>(); + + if !request_scopes.is_subset(&client_scopes) { + debug!(?client_id, "scopes aren't a subset"); + return Err(Error::Unauthorized); + } + + Ok(Authorizer { + issuer: &self.issuer, + client, + query, + state, + }) + } +} + +pub struct Authorizer<'a, I> { + issuer: &'a I, + client: Client<'a>, + query: ParamStorage<&'a str, &'a str>, + state: Option<&'a str>, +} + +impl<'a, I> Authorizer<'a, I> +where + I: Issuer, +{ + pub fn client(&self) -> &Client<'a> { + &self.client + } + + pub fn query(&self) -> &ParamStorage<&'a str, &'a str> { + &self.query + } + + pub async fn accept(self, user_id: I::UserId, scopes: &[&str]) -> http::Response<()> { + let code = self + .issuer + .issue_code(user_id, self.client.client_id, scopes) + .await + .unwrap(); + + let mut url = url::Url::parse(&self.client.redirect_uri).unwrap(); + url.query_pairs_mut().append_pair("code", &code); + + if let Some(state) = self.state { + url.query_pairs_mut().append_pair("state", state); + } + + http::Response::builder() + .header(http::header::LOCATION, url.as_str()) + .status(http::StatusCode::FOUND) + .body(()) + .unwrap() + } + + pub async fn deny(self) -> http::Response<()> { + todo!(); + } +} diff --git a/lib/komainu/src/lib.rs b/lib/komainu/src/lib.rs index 7e3075624..11e76bc8e 100644 --- a/lib/komainu/src/lib.rs +++ b/lib/komainu/src/lib.rs @@ -9,10 +9,11 @@ use strum::AsRefStr; pub use self::error::{Error, Result}; pub use self::params::ParamStorage; -mod authorize; mod error; mod params; +pub mod authorize; + trait OptionExt { fn or_missing_param(self) -> Result; } @@ -47,17 +48,6 @@ pub trait ClientExtractor { ) -> impl Future>> + Send; } -pub trait AuthIssuer { - type UserId; - - fn issue_code( - &self, - user_id: Self::UserId, - client_id: &str, - scopes: &[&str], - ) -> impl Future> + Send; -} - #[derive(AsRefStr)] #[strum(serialize_all = "snake_case")] pub enum OAuthError { @@ -78,117 +68,3 @@ fn get_from_either<'a>( ) -> Option<&'a str> { left.get(key).or_else(|| right.get(key)).map(|item| &**item) } - -pub struct AuthorizerExtractor { - // pls do not use ai for this, even if the type alias implies it. - // kthx bestie. bussi aufs bauchi. - auth_issuer: AI, - client_extractor: CE, -} - -impl AuthorizerExtractor -where - CE: ClientExtractor, -{ - pub fn new(auth_issuer: AI, client_extractor: CE) -> Self { - Self { - auth_issuer, - client_extractor, - } - } - - pub async fn extract<'a>(&'a self, req: &'a http::Request<()>) -> Result> { - let query: ParamStorage<&str, &str> = - serde_urlencoded::from_str(req.uri().query().or_missing_param()?) - .map_err(Error::query)?; - - // TODO: Load client and verify the parameters (client ID, client secret, redirect URI, scopes, etc.) check out - // Error out if that's not the case - // - // Check the grant_type, let the client access it _somehow_ - // - // Give the user some kind of "state" parameter, preferably typed, so they can store the authenticated user, and their - // consent answer. - - let client_id = query.get("client_id").or_missing_param()?; - let response_type = query.get("response_type").or_missing_param()?; - if *response_type != "code" { - debug!(?client_id, "response_type not set to \"code\""); - return Err(Error::Unauthorized); - } - - let scope = query.get("scope").or_missing_param()?; - let redirect_uri = query.get("redirect_uri").or_missing_param()?; - let state = query.get("state").map(|state| &**state); - - let client = self.client_extractor.extract(client_id, None).await?; - if client.redirect_uri != *redirect_uri { - debug!(?client_id, "redirect uri doesn't match"); - return Err(Error::Unauthorized); - } - - let request_scopes = scope.split_whitespace().collect::>(); - let client_scopes = client - .scopes - .iter() - .map(|scope| &**scope) - .collect::>(); - - if !request_scopes.is_subset(&client_scopes) { - debug!(?client_id, "scopes aren't a subset"); - return Err(Error::Unauthorized); - } - - Ok(Authorizer { - auth_issuer: &self.auth_issuer, - client, - query, - state, - }) - } -} - -pub struct Authorizer<'a, AI> { - auth_issuer: &'a AI, - client: Client<'a>, - query: ParamStorage<&'a str, &'a str>, - state: Option<&'a str>, -} - -impl<'a, AI> Authorizer<'a, AI> -where - AI: AuthIssuer, -{ - pub fn client(&self) -> &Client<'a> { - &self.client - } - - pub fn query(&self) -> &ParamStorage<&'a str, &'a str> { - &self.query - } - - pub async fn accept(self, user_id: AI::UserId, scopes: &[&str]) -> http::Response<()> { - let code = self - .auth_issuer - .issue_code(user_id, self.client.client_id, scopes) - .await - .unwrap(); - - let mut url = url::Url::parse(&self.client.redirect_uri).unwrap(); - url.query_pairs_mut().append_pair("code", &code); - - if let Some(state) = self.state { - url.query_pairs_mut().append_pair("state", state); - } - - http::Response::builder() - .header(http::header::LOCATION, url.as_str()) - .status(http::StatusCode::FOUND) - .body(()) - .unwrap() - } - - pub async fn deny(self) -> http::Response<()> { - todo!(); - } -}