Skip to content

Commit

Permalink
progress
Browse files Browse the repository at this point in the history
  • Loading branch information
aumetra committed Dec 7, 2024
1 parent 713f08e commit 5edaec2
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 24 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions lib/komainu/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ serde.workspace = true
serde_urlencoded.workspace = true
sonic-rs.workspace = true
thiserror.workspace = true
tracing.workspace = true

[dev-dependencies]
serde_test.workspace = true
Expand Down
6 changes: 6 additions & 0 deletions lib/komainu/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ pub enum Error {
#[error("Malformed body")]
Body(#[source] BoxError),

#[error("Missing parameter")]
MissingParam,

#[error("Malformed query")]
Query(#[source] BoxError),

#[error("Request is unauthorized")]
Unauthorized,
}

impl Error {
Expand Down
145 changes: 121 additions & 24 deletions lib/komainu/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,47 +1,144 @@
#[macro_use]
extern crate tracing;

use bytes::Bytes;
use headers::HeaderMapExt;
use std::collections::HashSet;
use std::{borrow::Cow, future::Future};

pub use self::error::{Error, Result};
pub use self::params::ParamStorage;

mod error;
mod params;

pub struct Authorizer<'a> {
request: &'a http::Request<Bytes>,
query: ParamStorage<&'a str, &'a str>,
body: ParamStorage<&'a str, &'a str>,
trait OptionExt<T> {
fn or_missing_param(self) -> Result<T>;
}

impl<'a> Authorizer<'a> {
pub fn extract(req: &'a http::Request<Bytes>) -> Result<Self> {
let body = req.body();

// TECHNICALLY the body should only be URL-encoded.
// Practically implementations like Mastodon and Pleroma allow it.
// Therefore I really don't care what some weird nerds say, this is going in.
let body = if req.headers().typed_get::<headers::ContentType>()
== Some(headers::ContentType::json())
{
sonic_rs::from_slice(body).map_err(Error::body)
} else {
serde_urlencoded::from_bytes(body).map_err(Error::body)
}?;
impl<T> OptionExt<T> for Option<T> {
#[inline]
fn or_missing_param(self) -> Result<T> {
self.ok_or(Error::MissingParam)
}
}

// TODO: Refactor into `AuthorizerExtractor` and `Authorizer`
//
// `AuthorizerExtractor` contains the `ClientExtractor`, so we can load client info.
// `Authorizer` is the handle passed to the consumer to accept or deny the request.
// Unlike `oxide-auth`, we won't force the user to implement a trait here, the flow better integrates with a simple function.
//
// Because we use native async traits where needed, we can't box the traits (not that we want to), so at least the compiler can inline stuff well

pub struct Client<'a> {
client_id: &'a str,
client_secret: &'a str,
scopes: Cow<'a, [Cow<'a, str>]>,
redirect_uri: Cow<'a, str>,
}

pub trait ClientExtractor {
fn extract(
&self,
client_id: &str,
client_secret: Option<&str>,
) -> impl Future<Output = Result<Client<'_>>> + Send;
}

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

#[inline]
fn get_from_either<'a>(
key: &str,
left: &'a ParamStorage<&str, &str>,
right: &'a ParamStorage<&str, &str>,
) -> Option<&'a str> {
left.get(key).or_else(|| right.get(key)).map(|item| &**item)
}

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

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

Ok(Self {
body,
// 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, preferrably typed, so they can store the authenticated user, and their

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

View workflow job for this annotation

GitHub Actions / Spell-check repository source

"preferrably" should be "preferably".
// consent answer.

// attempt to extract client data from the HTTP basic authorization
// if it doesnt succeed, load the data from the query or body
let client_id = query.get("client_id").or_missing_param()?;
let grant_type = query.get("grant_type").or_missing_param()?;

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, "scopes aren't a subset");
return Err(Error::Unauthorized);
}

Ok(Authorizer {
client,
grant_type,
query,
request: req,
state,
})
}
}

pub struct Authorizer<'a> {
client: Client<'a>,
grant_type: &'a str,
query: ParamStorage<&'a str, &'a str>,
state: Option<&'a str>,
}

impl<'a> Authorizer<'a> {
pub fn client(&self) -> &Client<'a> {
&self.client
}

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

pub async fn accept<UID>(self, user_id: UID) -> http::Response<()> {
// TODO: Call an issuer to issue an access token for a particular user
// Construct the callback url
// Construct a redirect HTTP response UwU

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

Expand Down

0 comments on commit 5edaec2

Please sign in to comment.