Skip to content

Commit

Permalink
move file to mod
Browse files Browse the repository at this point in the history
  • Loading branch information
aumetra committed Dec 12, 2024
1 parent adb0b75 commit af2c485
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 126 deletions.
129 changes: 129 additions & 0 deletions lib/komainu/src/authorize.rs
Original file line number Diff line number Diff line change
@@ -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<Output = Result<String>> + Send;
}

pub struct AuthorizerExtractor<I, CE> {
issuer: I,
client_extractor: CE,
}

impl<I, CE> AuthorizerExtractor<I, CE>
where
CE: ClientExtractor,
{
pub fn new(issuer: I, client_extractor: CE) -> Self {
Self {
issuer,
client_extractor,
}
}

Check warning on line 33 in lib/komainu/src/authorize.rs

View check run for this annotation

Codecov / codecov/patch

lib/komainu/src/authorize.rs#L28-L33

Added lines #L28 - L33 were not covered by tests

pub async fn extract<'a>(&'a self, req: &'a http::Request<()>) -> Result<Authorizer<'a, I>> {
let query: ParamStorage<&str, &str> =
serde_urlencoded::from_str(req.uri().query().or_missing_param()?)
.map_err(Error::query)?;

Check warning on line 38 in lib/komainu/src/authorize.rs

View check run for this annotation

Codecov / codecov/patch

lib/komainu/src/authorize.rs#L35-L38

Added lines #L35 - L38 were not covered by tests

// 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);
}

Check warning on line 53 in lib/komainu/src/authorize.rs

View check run for this annotation

Codecov / codecov/patch

lib/komainu/src/authorize.rs#L48-L53

Added lines #L48 - L53 were not covered by tests

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);

Check warning on line 57 in lib/komainu/src/authorize.rs

View check run for this annotation

Codecov / codecov/patch

lib/komainu/src/authorize.rs#L55-L57

Added lines #L55 - L57 were not covered by tests

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::<HashSet<_>>();
let client_scopes = client
.scopes
.iter()
.map(|scope| &**scope)
.collect::<HashSet<_>>();

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,
})
}

Check warning on line 83 in lib/komainu/src/authorize.rs

View check run for this annotation

Codecov / codecov/patch

lib/komainu/src/authorize.rs#L59-L83

Added lines #L59 - L83 were not covered by tests
}

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
}

Check warning on line 99 in lib/komainu/src/authorize.rs

View check run for this annotation

Codecov / codecov/patch

lib/komainu/src/authorize.rs#L97-L99

Added lines #L97 - L99 were not covered by tests

pub fn query(&self) -> &ParamStorage<&'a str, &'a str> {
&self.query
}

Check warning on line 103 in lib/komainu/src/authorize.rs

View check run for this annotation

Codecov / codecov/patch

lib/komainu/src/authorize.rs#L101-L103

Added lines #L101 - L103 were not covered by tests

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);

Check warning on line 113 in lib/komainu/src/authorize.rs

View check run for this annotation

Codecov / codecov/patch

lib/komainu/src/authorize.rs#L105-L113

Added lines #L105 - L113 were not covered by tests

if let Some(state) = self.state {
url.query_pairs_mut().append_pair("state", state);
}

Check warning on line 117 in lib/komainu/src/authorize.rs

View check run for this annotation

Codecov / codecov/patch

lib/komainu/src/authorize.rs#L115-L117

Added lines #L115 - L117 were not covered by tests

http::Response::builder()
.header(http::header::LOCATION, url.as_str())
.status(http::StatusCode::FOUND)
.body(())
.unwrap()
}

Check warning on line 124 in lib/komainu/src/authorize.rs

View check run for this annotation

Codecov / codecov/patch

lib/komainu/src/authorize.rs#L119-L124

Added lines #L119 - L124 were not covered by tests

pub async fn deny(self) -> http::Response<()> {
todo!();

Check warning on line 127 in lib/komainu/src/authorize.rs

View check run for this annotation

Codecov / codecov/patch

lib/komainu/src/authorize.rs#L126-L127

Added lines #L126 - L127 were not covered by tests
}
}
128 changes: 2 additions & 126 deletions lib/komainu/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
fn or_missing_param(self) -> Result<T>;
}
Expand Down Expand Up @@ -47,17 +48,6 @@ pub trait ClientExtractor {
) -> impl Future<Output = Result<Client<'_>>> + Send;
}

pub trait AuthIssuer {
type UserId;

fn issue_code(
&self,
user_id: Self::UserId,
client_id: &str,
scopes: &[&str],
) -> impl Future<Output = Result<String>> + Send;
}

#[derive(AsRefStr)]
#[strum(serialize_all = "snake_case")]
pub enum OAuthError {
Expand All @@ -78,117 +68,3 @@ fn get_from_either<'a>(
) -> Option<&'a str> {
left.get(key).or_else(|| right.get(key)).map(|item| &**item)
}

Check warning on line 70 in lib/komainu/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

lib/komainu/src/lib.rs#L64-L70

Added lines #L64 - L70 were not covered by tests

pub struct AuthorizerExtractor<AI, CE> {
// 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<AI, CE> AuthorizerExtractor<AI, CE>
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<Authorizer<'a, AI>> {
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::<HashSet<_>>();
let client_scopes = client
.scopes
.iter()
.map(|scope| &**scope)
.collect::<HashSet<_>>();

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!();
}
}

0 comments on commit af2c485

Please sign in to comment.