Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async refresh #98

Merged
merged 4 commits into from
Jul 14, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions oxide-auth-actix/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ futures = "0.1"
oxide-auth = { version = "0.5.0-preview.0", path = "../oxide-auth" }
serde_urlencoded = "0.5.1"
url = "1.7"
crossbeam-utils = "0.6.4"
Geobert marked this conversation as resolved.
Show resolved Hide resolved

[dev-dependencies]
base64 = "0.9"
Expand Down
30 changes: 29 additions & 1 deletion oxide-auth-async/src/endpoint/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use oxide_auth::endpoint::{OAuthError, Template, WebRequest, Scopes};
use async_trait::async_trait;
use oxide_auth::endpoint::{OAuthError, Template, WebRequest, OwnerConsent, PreGrant, Scopes};

// pub use crate::code_grant::authorization::Extension as AuthorizationExtension;
pub use crate::code_grant::access_token::Extension as AccessTokenExtension;
use crate::primitives::{Authorizer, Registrar, Issuer};

pub mod access_token;
pub mod refresh;
pub mod resource;

pub trait Endpoint<Request: WebRequest> {
Expand Down Expand Up @@ -75,3 +77,29 @@ pub trait Extension {
None
}
}

/// Checks consent with the owner of a resource, identified in a request.
///
/// See [`frontends::simple`] for an implementation that permits arbitrary functions.
///
/// [`frontends::simple`]: ../frontends/simple/endpoint/struct.FnSolicitor.html
#[async_trait(?Send)]
pub trait OwnerSolicitor<Request: WebRequest> {
/// Ensure that a user (resource owner) is currently authenticated (for example via a session
/// cookie) and determine if he has agreed to the presented grants.
async fn check_consent(
&mut self, req: &mut Request, pre_grant: &PreGrant,
) -> OwnerConsent<Request::Response>;
}

#[async_trait(?Send)]
impl<T, Request: WebRequest> OwnerSolicitor<Request> for T
where
T: oxide_auth::endpoint::OwnerSolicitor<Request> + ?Sized,
{
async fn check_consent(
&mut self, req: &mut Request, pre_grant: &PreGrant,
) -> OwnerConsent<Request::Response> {
oxide_auth::endpoint::OwnerSolicitor::check_consent(self, req, pre_grant)
}
}
227 changes: 227 additions & 0 deletions oxide-auth-async/src/endpoint/refresh.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
use std::{borrow::Cow, marker::PhantomData, str::from_utf8};

use oxide_auth::{
code_grant::refresh::{Error, Request},
endpoint::{WebRequest, WebResponse, OAuthError, QueryParameter, Template},
};

use super::Endpoint;
use crate::{
code_grant::refresh::{refresh, Endpoint as RefreshEndpoint},
primitives::{Issuer, Registrar},
};

/// Takes requests from clients to refresh their access tokens.
pub struct RefreshFlow<E, R>
where
E: Endpoint<R>,
R: WebRequest,
{
endpoint: WrappedRefresh<E, R>,
}

struct WrappedRefresh<E: Endpoint<R>, R: WebRequest> {
inner: E,
r_type: PhantomData<R>,
}

struct WrappedRequest<'a, R: WebRequest + 'a> {
/// Original request.
request: PhantomData<R>,

/// The query in the body.
body: Cow<'a, dyn QueryParameter + 'static>,

/// The authorization token.
authorization: Option<Authorization>,

/// An error if one occurred.
error: Option<Option<R::Error>>,
}

struct Authorization(String, Vec<u8>);

impl<E, R> RefreshFlow<E, R>
where
E: Endpoint<R>,
R: WebRequest,
{
/// Wrap the endpoint if it supports handling refresh requests.
///
/// Also binds the endpoint to the particular `WebRequest` type through the type system. The
/// endpoint needs to provide (return `Some`):
///
/// * a `Registrar` from `registrar`
/// * an `Issuer` from `issuer_mut`
///
/// ## Panics
///
/// Indirectly `execute` may panic when this flow is instantiated with an inconsistent
/// endpoint, for details see the documentation of `Endpoint` and `execute`. For
/// consistent endpoints, the panic is instead caught as an error here.
pub fn prepare(mut endpoint: E) -> Result<Self, E::Error> {
if endpoint.registrar().is_none() {
return Err(endpoint.error(OAuthError::PrimitiveError));
}

if endpoint.issuer_mut().is_none() {
return Err(endpoint.error(OAuthError::PrimitiveError));
}

Ok(RefreshFlow {
endpoint: WrappedRefresh {
inner: endpoint,
r_type: PhantomData,
},
})
}

pub async fn execute(&mut self, mut request: R) -> Result<R::Response, E::Error> {
let refreshed = refresh(&mut self.endpoint, &WrappedRequest::new(&mut request)).await;

let token = match refreshed {
Err(error) => return token_error(&mut self.endpoint.inner, &mut request, error),
Ok(token) => token,
};

let mut response = self.endpoint.inner.response(&mut request, Template::new_ok())?;
response
.body_json(&token.to_json())
.map_err(|err| self.endpoint.inner.web_error(err))?;
Ok(response)
}
}

fn token_error<E: Endpoint<R>, R: WebRequest>(
endpoint: &mut E, request: &mut R, error: Error,
) -> Result<R::Response, E::Error> {
Ok(match error {
Error::Invalid(mut json) => {
let mut response =
endpoint.response(request, Template::new_bad(Some(json.description())))?;
response.client_error().map_err(|err| endpoint.web_error(err))?;
response
.body_json(&json.to_json())
.map_err(|err| endpoint.web_error(err))?;
response
}
Error::Unauthorized(mut json, scheme) => {
let mut response = endpoint.response(
request,
Template::new_unauthorized(None, Some(json.description())),
)?;
response
.unauthorized(&scheme)
.map_err(|err| endpoint.web_error(err))?;
response
.body_json(&json.to_json())
.map_err(|err| endpoint.web_error(err))?;
response
}
Error::Primitive => {
// FIXME: give the context for restoration.
return Err(endpoint.error(OAuthError::PrimitiveError));
}
})
}

impl<'a, R: WebRequest + 'a> WrappedRequest<'a, R> {
pub fn new(request: &'a mut R) -> Self {
Self::new_or_fail(request).unwrap_or_else(Self::from_err)
}

fn new_or_fail(request: &'a mut R) -> Result<Self, Option<R::Error>> {
// If there is a header, it must parse correctly.
let authorization = match request.authheader() {
Err(err) => return Err(Some(err)),
Ok(Some(header)) => Self::parse_header(header).map(Some)?,
Ok(None) => None,
};

Ok(WrappedRequest {
request: PhantomData,
body: request.urlbody()?,
authorization,
error: None,
})
}

fn from_err(err: Option<R::Error>) -> Self {
WrappedRequest {
request: PhantomData,
body: Cow::Owned(Default::default()),
authorization: None,
error: Some(err),
}
}

fn parse_header(header: Cow<str>) -> Result<Authorization, Option<R::Error>> {
let authorization = {
if !header.starts_with("Basic ") {
return Err(None);
}

let combined = match base64::decode(&header[6..]) {
Err(_) => return Err(None),
Ok(vec) => vec,
};

let mut split = combined.splitn(2, |&c| c == b':');
let client_bin = match split.next() {
None => return Err(None),
Some(client) => client,
};
let passwd = match split.next() {
None => return Err(None),
Some(passwd64) => passwd64,
};

let client = match from_utf8(client_bin) {
Err(_) => return Err(None),
Ok(client) => client,
};

Authorization(client.to_string(), passwd.to_vec())
};

Ok(authorization)
}
}

impl<E: Endpoint<R>, R: WebRequest> RefreshEndpoint for WrappedRefresh<E, R> {
fn registrar(&self) -> &dyn Registrar {
self.inner.registrar().unwrap()
}

fn issuer(&mut self) -> &mut dyn Issuer {
self.inner.issuer_mut().unwrap()
}
}

impl<'a, R: WebRequest> Request for WrappedRequest<'a, R> {
fn valid(&self) -> bool {
self.error.is_none()
}

fn refresh_token(&self) -> Option<Cow<str>> {
self.body.unique_value("refresh_token")
}

fn authorization(&self) -> Option<(Cow<str>, Cow<[u8]>)> {
self.authorization
.as_ref()
.map(|auth| (auth.0.as_str().into(), auth.1.as_slice().into()))
}

fn scope(&self) -> Option<Cow<str>> {
self.body.unique_value("scope")
}

fn grant_type(&self) -> Option<Cow<str>> {
self.body.unique_value("grant_type")
}

fn extension(&self, key: &str) -> Option<Cow<str>> {
self.body.unique_value(key)
}
}
2 changes: 1 addition & 1 deletion oxide-auth-async/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,5 +214,5 @@ pub mod defaults {
// mod authorization;
mod access_token;
mod resource;
// mod refresh;
mod refresh;
// mod pkce;
Loading