use std::borrow::Cow;
use std::collections::HashMap;
use std::marker::PhantomData;
use std::fmt;
use std::error;
use super::backend::{AccessTokenRequest, CodeRef, CodeRequest, CodeError, ErrorUrl, IssuerError, IssuerRef};
use super::backend::{AccessError, GuardRequest, GuardRef};
use url::Url;
struct ClientParameter<'a> {
valid: bool,
client_id: Option<Cow<'a, str>>,
scope: Option<Cow<'a, str>>,
redirect_url: Option<Cow<'a, str>>,
state: Option<Cow<'a, str>>,
}
pub struct AuthenticationRequest {
pub client_id: String,
pub scope: String,
}
#[derive(Clone)]
pub enum Authentication {
Failed,
InProgress,
Authenticated(String),
}
struct AccessTokenParameter<'a> {
valid: bool,
client_id: Option<Cow<'a, str>>,
redirect_url: Option<Cow<'a, str>>,
grant_type: Option<Cow<'a, str>>,
code: Option<Cow<'a, str>>,
}
struct GuardParameter<'a> {
valid: bool,
token: Option<Cow<'a, str>>,
}
pub trait WebRequest {
type Response: WebResponse;
fn query(&mut self) -> Result<HashMap<String, Vec<String>>, ()>;
fn urlbody(&mut self) -> Result<&HashMap<String, Vec<String>>, ()>;
fn authheader(&mut self) -> Result<Option<Cow<str>>, ()>;
}
pub trait WebResponse where Self: Sized {
fn redirect(url: Url) -> Result<Self, OAuthError>;
fn text(text: &str) -> Result<Self, OAuthError>;
fn json(data: &str) -> Result<Self, OAuthError>;
fn redirect_error(target: ErrorUrl) -> Result<Self, OAuthError> {
Self::redirect(target.into())
}
fn as_client_error(self) -> Result<Self, OAuthError>;
fn as_unauthorized(self) -> Result<Self, OAuthError>;
fn with_authorization(self, kind: &str) -> Result<Self, OAuthError>;
}
pub trait OwnerAuthorizer {
type Request: WebRequest;
fn get_owner_authorization(&self, &mut Self::Request, AuthenticationRequest) -> Result<(Authentication, <Self::Request as WebRequest>::Response), OAuthError>;
}
pub struct AuthorizationFlow;
pub struct PreparedAuthorization<'l, Req> where
Req: WebRequest + 'l,
{
request: &'l mut Req,
urldecoded: ClientParameter<'l>,
}
fn extract_parameters(params: HashMap<String, Vec<String>>) -> ClientParameter<'static> {
let map = params.iter()
.filter(|&(_, v)| v.len() == 1)
.map(|(k, v)| (k.as_str(), v[0].as_str()))
.collect::<HashMap<&str, &str>>();
ClientParameter{
valid: true,
client_id: map.get("client_id").map(|client| client.to_string().into()),
scope: map.get("scope").map(|scope| scope.to_string().into()),
redirect_url: map.get("redirect_url").map(|url| url.to_string().into()),
state: map.get("state").map(|state| state.to_string().into()),
}
}
impl<'s> CodeRequest for ClientParameter<'s> {
fn valid(&self) -> bool { self.valid }
fn client_id(&self) -> Option<Cow<str>> { self.client_id.as_ref().map(|c| c.as_ref().into()) }
fn scope(&self) -> Option<Cow<str>> { self.scope.as_ref().map(|c| c.as_ref().into()) }
fn redirect_url(&self) -> Option<Cow<str>> { self.redirect_url.as_ref().map(|c| c.as_ref().into()) }
fn state(&self) -> Option<Cow<str>> { self.state.as_ref().map(|c| c.as_ref().into()) }
}
impl<'s> ClientParameter<'s> {
fn invalid() -> Self {
ClientParameter { valid: false, client_id: None, scope: None,
redirect_url: None, state: None }
}
}
impl AuthorizationFlow {
pub fn prepare<W: WebRequest>(incoming: &mut W) -> Result<PreparedAuthorization<W>, OAuthError> {
let urldecoded = incoming.query()
.map(extract_parameters)
.unwrap_or_else(|_| ClientParameter::invalid());
Ok(PreparedAuthorization{request: incoming, urldecoded})
}
pub fn handle<'c, Req, Auth>(granter: CodeRef<'c>, prepared: PreparedAuthorization<'c, Req>, page_handler: &Auth)
-> Result<Req::Response, OAuthError> where
Req: WebRequest,
Auth: OwnerAuthorizer<Request=Req>
{
let PreparedAuthorization { request: req, urldecoded } = prepared;
let negotiated = match granter.negotiate(&urldecoded) {
Err(CodeError::Ignore) => return Err(OAuthError::ParameterNegotiationFailed),
Err(CodeError::Redirect(url)) => return Req::Response::redirect_error(url),
Ok(v) => v,
};
let auth = AuthenticationRequest{
client_id: negotiated.negotiated().client_id.to_string(),
scope: negotiated.negotiated().scope.to_string(),
};
let authorization = match page_handler.get_owner_authorization(req, auth)? {
(Authentication::Failed, _)
=> negotiated.deny(),
(Authentication::InProgress, response)
=> return Ok(response),
(Authentication::Authenticated(owner), _)
=> negotiated.authorize(owner.into()),
};
let redirect_to = match authorization {
Err(CodeError::Ignore) => return Err(OAuthError::AuthorizationFailed),
Err(CodeError::Redirect(url)) => return Req::Response::redirect_error(url),
Ok(v) => v,
};
Req::Response::redirect(redirect_to)
}
}
pub struct GrantFlow;
pub struct PreparedGrant<'l, Req> where
Req: WebRequest + 'l,
{
params: AccessTokenParameter<'l>,
req: PhantomData<Req>,
}
fn extract_access_token<'l>(params: &'l HashMap<String, Vec<String>>) -> AccessTokenParameter<'l> {
let map = params.iter()
.filter(|&(_, v)| v.len() == 1)
.map(|(k, v)| (k.as_str(), v[0].as_str()))
.collect::<HashMap<_, _>>();
AccessTokenParameter {
valid: true,
client_id: map.get("client_id").map(|v| (*v).into()),
code: map.get("code").map(|v| (*v).into()),
redirect_url: map.get("redirect_url").map(|v| (*v).into()),
grant_type: map.get("grant_type").map(|v| (*v).into()),
}
}
impl<'l> AccessTokenRequest for AccessTokenParameter<'l> {
fn valid(&self) -> bool { self.valid }
fn code(&self) -> Option<Cow<str>> { self.code.clone() }
fn client_id(&self) -> Option<Cow<str>> { self.client_id.clone() }
fn redirect_url(&self) -> Option<Cow<str>> { self.redirect_url.clone() }
fn grant_type(&self) -> Option<Cow<str>> { self.grant_type.clone() }
fn authorization(&self) -> Option<(Cow<str>, Cow<str>)> { None }
}
impl<'l> AccessTokenParameter<'l> {
fn invalid() -> Self {
AccessTokenParameter { valid: false, code: None, client_id: None, redirect_url: None,
grant_type: None, }
}
}
impl GrantFlow {
pub fn prepare<W: WebRequest>(req: &mut W) -> Result<PreparedGrant<W>, OAuthError> {
let params = req.urlbody()
.map(extract_access_token)
.unwrap_or_else(|_| AccessTokenParameter::invalid());
Ok(PreparedGrant { params: params, req: PhantomData })
}
pub fn handle<Req>(mut issuer: IssuerRef, prepared: PreparedGrant<Req>)
-> Result<Req::Response, OAuthError> where Req: WebRequest
{
let PreparedGrant { params, .. } = prepared;
match issuer.use_code(¶ms) {
Err(IssuerError::Invalid(json_data))
=> return Req::Response::json(&json_data.to_json())?.as_client_error(),
Err(IssuerError::Unauthorized(json_data, scheme))
=> return Req::Response::json(&json_data.to_json())?.as_unauthorized()?.with_authorization(&scheme),
Ok(token) => Req::Response::json(&token.to_json()),
}
}
}
pub struct AccessFlow;
pub struct PreparedAccess<'l, Req> where
Req: WebRequest + 'l,
{
params: GuardParameter<'l>,
req: PhantomData<Req>,
}
impl<'l> GuardRequest for GuardParameter<'l> {
fn valid(&self) -> bool { self.valid }
fn token(&self) -> Option<Cow<str>> { self.token.clone() }
}
impl<'l> GuardParameter<'l> {
fn invalid() -> Self {
GuardParameter { valid: false, token: None }
}
}
impl AccessFlow {
pub fn prepare<W: WebRequest>(req: &mut W) -> Result<PreparedAccess<W>, OAuthError> {
let params = req.authheader()
.map(|auth| GuardParameter { valid: true, token: auth })
.unwrap_or_else(|_| GuardParameter::invalid());
Ok(PreparedAccess { params: params, req: PhantomData })
}
pub fn handle<Req>(guard: GuardRef, prepared: PreparedAccess<Req>)
-> Result<(), OAuthError> where Req: WebRequest {
guard.protect(&prepared.params).map_err(|err| {
match err {
AccessError::InvalidRequest => OAuthError::BadRequest("Invalid format".to_string()),
AccessError::AccessDenied => OAuthError::AuthorizationFailed,
}
})
}
}
#[derive(Debug)]
pub enum OAuthError {
ParameterNegotiationFailed,
AuthorizationFailed,
BadRequest(String),
Other(String),
}
impl fmt::Display for OAuthError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
fmt.write_str("OAuthError")
}
}
impl error::Error for OAuthError {
fn description(&self) -> &str {
"OAuthError"
}
}