use super::scope::Scope;
use std::borrow::Cow;
use std::collections::HashMap;
use url::Url;
use ring::{constant_time, digest};
use ring::error::Unspecified;
pub trait Registrar {
fn bound_redirect<'a>(&'a self, bound: ClientUrl<'a>) -> Result<BoundClient<'a>, RegistrarError>;
fn client(&self, client_id: &str) -> Option<&Client>;
}
pub struct ClientUrl<'a> {
pub client_id: Cow<'a, str>,
pub redirect_uri: Option<Cow<'a, Url>>,
}
pub struct BoundClient<'a> {
pub client_id: Cow<'a, str>,
pub redirect_uri: Cow<'a, Url>,
pub client: &'a Client,
}
#[derive(Clone)]
pub struct PreGrant {
pub client_id: String,
pub redirect_uri: Url,
pub scope: Scope,
}
pub enum RegistrarError {
Unregistered,
MismatchedRedirect,
UnauthorizedClient,
}
pub struct Client {
client_id: String,
redirect_uri: Url,
default_scope: Scope,
client_type: ClientType,
}
enum ClientType {
Public,
Confidential{ passdata: Vec<u8>, },
}
pub struct ClientMap {
clients: HashMap<String, Client>,
}
impl<'a> BoundClient<'a> {
pub fn negotiate(self, _scope: Option<Scope>) -> PreGrant {
PreGrant {
client_id: self.client_id.into_owned(),
redirect_uri: self.redirect_uri.into_owned(),
scope: self.client.default_scope.clone(),
}
}
}
impl Client {
pub fn public(client_id: &str, redirect_uri: Url, default_scope: Scope) -> Client {
Client { client_id: client_id.to_string(), redirect_uri, default_scope, client_type: ClientType::Public }
}
pub fn confidential(client_id: &str, redirect_uri: Url, default_scope: Scope, passphrase: &[u8]) -> Client {
let passdata = SHA256Policy.store(client_id, passphrase);
Client {
client_id: client_id.to_string(),
redirect_uri,
default_scope,
client_type: ClientType::Confidential { passdata },
}
}
pub fn check_authentication(&self, passphrase: Option<&[u8]>) -> Result<&Self, Unspecified> {
match (passphrase, &self.client_type) {
(None, &ClientType::Public) => Ok(self),
(Some(provided), &ClientType::Confidential{ passdata: ref stored })
=> SHA256Policy.check(&self.client_id, provided, stored).map(|()| self),
_ => return Err(Unspecified)
}
}
}
trait PasswordPolicy {
fn store(&self, client_id: &str, passphrase: &[u8]) -> Vec<u8>;
fn check(&self, client_id: &str, passphrase: &[u8], stored: &[u8]) -> Result<(), Unspecified>;
}
#[deprecated(since="0.1.0-alpha.1", note="Should be replaced with argon2 as soon as possible")]
struct SHA256Policy;
#[allow(deprecated)]
impl PasswordPolicy for SHA256Policy {
fn store(&self, client_id: &str, passphrase: &[u8]) -> Vec<u8> {
let mut context = digest::Context::new(&digest::SHA256);
context.update(client_id.as_bytes());
context.update(passphrase);
context.finish().as_ref().to_vec()
}
fn check(&self, client_id: &str, passphrase: &[u8], stored: &[u8]) -> Result<(), Unspecified> {
let mut context = digest::Context::new(&digest::SHA256);
context.update(client_id.as_bytes());
context.update(passphrase);
constant_time::verify_slices_are_equal(context.finish().as_ref(), stored)
}
}
impl ClientMap {
pub fn new() -> ClientMap {
ClientMap { clients: HashMap::new() }
}
pub fn register_client(&mut self, client: Client) {
self.clients.insert(client.client_id.clone(), client);
}
}
impl Registrar for ClientMap {
fn bound_redirect<'a>(&'a self, bound: ClientUrl<'a>) -> Result<BoundClient<'a>, RegistrarError> {
let client = match self.clients.get(bound.client_id.as_ref()) {
None => return Err(RegistrarError::Unregistered),
Some(stored) => stored
};
match bound.redirect_uri {
None => (),
Some(ref url) if url.as_ref().as_str() == client.redirect_uri.as_str() => (),
_ => return Err(RegistrarError::MismatchedRedirect),
}
Ok(BoundClient{
client_id: bound.client_id,
redirect_uri: bound.redirect_uri.unwrap_or_else(
|| Cow::Owned(client.redirect_uri.clone())),
client: client})
}
fn client(&self, client_id: &str) -> Option<&Client> {
self.clients.get(client_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
pub fn simple_test_suite<Reg, RegFn>(registrar: &mut Reg, register: RegFn)
where
Reg: Registrar,
RegFn: Fn(&mut Reg, Client)
{
let public_id = "PrivateClientId";
let client_url = "https://example.com";
let private_id = "PublicClientId";
let private_passphrase = b"WOJJCcS8WyS2aGmJK6ZADg==";
let public_client = Client::public(public_id, client_url.parse().unwrap(),
"default".parse().unwrap());
register(registrar, public_client);
{
let recovered_client = registrar.client(public_id)
.expect("Registered client not available");
recovered_client.check_authentication(None)
.expect("Authorization of public client has changed");
}
let private_client = Client::confidential(private_id, client_url.parse().unwrap(),
"default".parse().unwrap(), private_passphrase);
register(registrar, private_client);
{
let recovered_client = registrar.client(private_id)
.expect("Registered client not available");
recovered_client.check_authentication(Some(private_passphrase))
.expect("Authorization of private client has changed");
}
}
#[test]
fn public_client() {
let client = Client::public("ClientId", "https://example.com".parse().unwrap(),
"default".parse().unwrap());
assert!(client.check_authentication(None).is_ok());
assert!(client.check_authentication(Some(b"")).is_err());
}
#[test]
fn confidential_client() {
let pass = b"AB3fAj6GJpdxmEVeNCyPoA==";
let client = Client::confidential("ClientId", "https://example.com".parse().unwrap(),
"default".parse().unwrap(), pass);
assert!(client.check_authentication(None).is_err());
assert!(client.check_authentication(Some(pass)).is_ok());
assert!(client.check_authentication(Some(b"not the passphrase")).is_err());
assert!(client.check_authentication(Some(b"")).is_err());
}
#[test]
fn client_map() {
let mut client_map = ClientMap::new();
simple_test_suite(&mut client_map, ClientMap::register_client);
}
}