use super::scope::Scope;
use std::borrow::Cow;
use std::cmp;
use std::collections::HashMap;
use std::fmt;
use std::iter::{Extend, FromIterator};
use std::rc::Rc;
use std::sync::{Arc, MutexGuard, RwLockWriteGuard};
use argon2::{self, Config};
use once_cell::sync::Lazy;
use rand::{RngCore, thread_rng};
use serde::{Deserialize, Serialize};
use url::{Url, ParseError as ParseUrlError};
pub trait Registrar {
fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result<BoundClient<'a>, RegistrarError>;
fn negotiate(&self, client: BoundClient, scope: Option<Scope>) -> Result<PreGrant, RegistrarError>;
fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError>;
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum RegisteredUrl {
Exact(ExactUrl),
Semantic(Url),
IgnorePortOnLocalhost(IgnoreLocalPortUrl),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ExactUrl(String);
impl<'de> Deserialize<'de> for ExactUrl {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let string: &str = Deserialize::deserialize(deserializer)?;
core::str::FromStr::from_str(&string).map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IgnoreLocalPortUrl(IgnoreLocalPortUrlInternal);
#[derive(Clone, Debug, PartialEq, Eq)]
enum IgnoreLocalPortUrlInternal {
Exact(String),
Local(Url),
}
impl Serialize for IgnoreLocalPortUrl {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for IgnoreLocalPortUrl {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let string: &str = Deserialize::deserialize(deserializer)?;
Self::new(string).map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Debug)]
pub struct ClientUrl<'a> {
pub client_id: Cow<'a, str>,
pub redirect_uri: Option<Cow<'a, ExactUrl>>,
}
#[derive(Clone, Debug)]
pub struct BoundClient<'a> {
pub client_id: Cow<'a, str>,
pub redirect_uri: Cow<'a, RegisteredUrl>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PreGrant {
pub client_id: String,
pub redirect_uri: RegisteredUrl,
pub scope: Scope,
}
#[derive(Clone, Debug)]
pub enum RegistrarError {
Unspecified,
PrimitiveError,
}
#[derive(Clone, Debug)]
pub struct Client {
client_id: String,
redirect_uri: RegisteredUrl,
additional_redirect_uris: Vec<RegisteredUrl>,
default_scope: Scope,
client_type: ClientType,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EncodedClient {
pub client_id: String,
pub redirect_uri: RegisteredUrl,
pub additional_redirect_uris: Vec<RegisteredUrl>,
pub default_scope: Scope,
pub encoded_client: ClientType,
}
pub struct RegisteredClient<'a> {
client: &'a EncodedClient,
policy: &'a dyn PasswordPolicy,
}
#[derive(Clone, Serialize, Deserialize)]
pub enum ClientType {
Public,
Confidential {
passdata: Vec<u8>,
},
}
#[derive(Default)]
pub struct ClientMap {
clients: HashMap<String, EncodedClient>,
password_policy: Option<Box<dyn PasswordPolicy>>,
}
impl fmt::Debug for ClientType {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match self {
ClientType::Public => write!(f, "<public>"),
ClientType::Confidential { .. } => write!(f, "<confidential>"),
}
}
}
impl RegisteredUrl {
pub fn as_str(&self) -> &str {
match self {
RegisteredUrl::Exact(exact) => &exact.0,
RegisteredUrl::Semantic(url) => url.as_str(),
RegisteredUrl::IgnorePortOnLocalhost(url) => url.as_str(),
}
}
pub fn to_url(&self) -> Url {
match self {
RegisteredUrl::Exact(exact) => exact.to_url(),
RegisteredUrl::Semantic(url) => url.clone(),
RegisteredUrl::IgnorePortOnLocalhost(url) => url.to_url(),
}
}
pub fn into_url(self) -> Url {
self.into()
}
}
impl From<Url> for RegisteredUrl {
fn from(url: Url) -> Self {
RegisteredUrl::Semantic(url)
}
}
impl From<ExactUrl> for RegisteredUrl {
fn from(url: ExactUrl) -> Self {
RegisteredUrl::Exact(url)
}
}
impl From<IgnoreLocalPortUrl> for RegisteredUrl {
fn from(url: IgnoreLocalPortUrl) -> Self {
RegisteredUrl::IgnorePortOnLocalhost(url)
}
}
impl From<RegisteredUrl> for Url {
fn from(url: RegisteredUrl) -> Self {
match url {
RegisteredUrl::Exact(exact) => exact.0.parse().expect("was validated"),
RegisteredUrl::Semantic(url) => url,
RegisteredUrl::IgnorePortOnLocalhost(url) => url.to_url(),
}
}
}
impl cmp::PartialEq<ExactUrl> for RegisteredUrl {
fn eq(&self, exact: &ExactUrl) -> bool {
match self {
RegisteredUrl::Exact(url) => url == exact,
RegisteredUrl::Semantic(url) => *url == exact.to_url(),
RegisteredUrl::IgnorePortOnLocalhost(url) => url == &IgnoreLocalPortUrl::from(exact),
}
}
}
impl cmp::PartialEq<IgnoreLocalPortUrl> for RegisteredUrl {
fn eq(&self, ign_lport: &IgnoreLocalPortUrl) -> bool {
match self {
RegisteredUrl::Exact(url) => ign_lport == &IgnoreLocalPortUrl::from(url),
RegisteredUrl::Semantic(url) => ign_lport == &IgnoreLocalPortUrl::from(url.clone()),
RegisteredUrl::IgnorePortOnLocalhost(url) => ign_lport == url,
}
}
}
impl cmp::PartialEq<Url> for RegisteredUrl {
fn eq(&self, semantic: &Url) -> bool {
self.to_url() == *semantic
}
}
impl fmt::Display for RegisteredUrl {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
RegisteredUrl::Exact(url) => write!(f, "{}", url.to_url()),
RegisteredUrl::Semantic(url) => write!(f, "{}", url),
RegisteredUrl::IgnorePortOnLocalhost(url) => write!(f, "{}", url.to_url()),
}
}
}
impl ExactUrl {
pub fn new(url: String) -> Result<Self, ParseUrlError> {
let _: Url = url.parse()?;
Ok(ExactUrl(url))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn to_url(&self) -> Url {
self.0.parse().expect("was validated")
}
}
impl core::str::FromStr for ExactUrl {
type Err = ParseUrlError;
fn from_str(st: &str) -> Result<Self, Self::Err> {
let _: Url = st.parse()?;
Ok(ExactUrl(st.to_string()))
}
}
impl IgnoreLocalPortUrl {
pub fn new<'a, S: Into<Cow<'a, str>>>(url: S) -> Result<Self, ParseUrlError> {
let url: Cow<'a, str> = url.into();
let mut parsed: Url = url.parse()?;
match parsed.host_str() {
Some("localhost") => {
let _ = parsed.set_port(None);
Ok(IgnoreLocalPortUrl(IgnoreLocalPortUrlInternal::Local(parsed)))
}
_ => Ok(IgnoreLocalPortUrl(IgnoreLocalPortUrlInternal::Exact(
url.into_owned(),
))),
}
}
pub fn as_str(&self) -> &str {
match &self.0 {
IgnoreLocalPortUrlInternal::Exact(url) => url.as_str(),
IgnoreLocalPortUrlInternal::Local(url) => url.as_str(),
}
}
pub fn to_url(&self) -> Url {
match &self.0 {
IgnoreLocalPortUrlInternal::Exact(url) => url.parse().expect("was validated"),
IgnoreLocalPortUrlInternal::Local(url) => url.clone(),
}
}
}
impl From<ExactUrl> for IgnoreLocalPortUrl {
#[inline]
fn from(exact_url: ExactUrl) -> Self {
IgnoreLocalPortUrl::new(exact_url.0).expect("was validated")
}
}
impl<'e> From<&'e ExactUrl> for IgnoreLocalPortUrl {
#[inline]
fn from(exact_url: &'e ExactUrl) -> Self {
IgnoreLocalPortUrl::new(exact_url.as_str()).expect("was validated")
}
}
impl From<Url> for IgnoreLocalPortUrl {
fn from(mut url: Url) -> Self {
if url.host_str() == Some("localhost") {
let _ = url.set_port(None);
IgnoreLocalPortUrl(IgnoreLocalPortUrlInternal::Local(url))
} else {
IgnoreLocalPortUrl(IgnoreLocalPortUrlInternal::Exact(url.into()))
}
}
}
impl core::str::FromStr for IgnoreLocalPortUrl {
type Err = ParseUrlError;
#[inline]
fn from_str(st: &str) -> Result<Self, Self::Err> {
IgnoreLocalPortUrl::new(st)
}
}
impl Client {
pub fn public(client_id: &str, redirect_uri: RegisteredUrl, default_scope: Scope) -> Client {
Client {
client_id: client_id.to_string(),
redirect_uri,
additional_redirect_uris: vec![],
default_scope,
client_type: ClientType::Public,
}
}
pub fn confidential(
client_id: &str, redirect_uri: RegisteredUrl, default_scope: Scope, passphrase: &[u8],
) -> Client {
Client {
client_id: client_id.to_string(),
redirect_uri,
additional_redirect_uris: vec![],
default_scope,
client_type: ClientType::Confidential {
passdata: passphrase.to_owned(),
},
}
}
pub fn with_additional_redirect_uris(mut self, uris: Vec<RegisteredUrl>) -> Self {
self.additional_redirect_uris = uris;
self
}
pub fn encode(self, policy: &dyn PasswordPolicy) -> EncodedClient {
let encoded_client = match self.client_type {
ClientType::Public => ClientType::Public,
ClientType::Confidential { passdata: passphrase } => ClientType::Confidential {
passdata: policy.store(&self.client_id, &passphrase),
},
};
EncodedClient {
client_id: self.client_id,
redirect_uri: self.redirect_uri,
additional_redirect_uris: self.additional_redirect_uris,
default_scope: self.default_scope,
encoded_client,
}
}
}
impl<'a> RegisteredClient<'a> {
pub fn new(client: &'a EncodedClient, policy: &'a dyn PasswordPolicy) -> Self {
RegisteredClient { client, policy }
}
pub fn check_authentication(&self, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> {
match (passphrase, &self.client.encoded_client) {
(None, &ClientType::Public) => Ok(()),
(Some(provided), &ClientType::Confidential { passdata: ref stored }) => {
self.policy.check(&self.client.client_id, provided, stored)
}
_ => Err(RegistrarError::Unspecified),
}
}
}
impl cmp::PartialOrd<Self> for PreGrant {
fn partial_cmp(&self, rhs: &PreGrant) -> Option<cmp::Ordering> {
if (&self.client_id, &self.redirect_uri) != (&rhs.client_id, &rhs.redirect_uri) {
None
} else {
self.scope.partial_cmp(&rhs.scope)
}
}
}
pub trait PasswordPolicy: Send + Sync {
fn store(&self, client_id: &str, passphrase: &[u8]) -> Vec<u8>;
fn check(&self, client_id: &str, passphrase: &[u8], stored: &[u8]) -> Result<(), RegistrarError>;
}
#[derive(Clone, Debug, Default)]
pub struct Argon2 {
_private: (),
}
impl PasswordPolicy for Argon2 {
fn store(&self, client_id: &str, passphrase: &[u8]) -> Vec<u8> {
let config = Config {
ad: client_id.as_bytes(),
secret: &[],
..Config::rfc9106_low_mem()
};
let mut salt = vec![0; 32];
thread_rng()
.try_fill_bytes(salt.as_mut_slice())
.expect("Failed to generate password salt");
let encoded = argon2::hash_encoded(passphrase, &salt, &config);
encoded.unwrap().as_bytes().to_vec()
}
fn check(&self, client_id: &str, passphrase: &[u8], stored: &[u8]) -> Result<(), RegistrarError> {
let hash = String::from_utf8(stored.to_vec()).map_err(|_| RegistrarError::PrimitiveError)?;
let valid = argon2::verify_encoded_ext(&hash, passphrase, &[], client_id.as_bytes())
.map_err(|_| RegistrarError::PrimitiveError)?;
match valid {
true => Ok(()),
false => Err(RegistrarError::Unspecified),
}
}
}
static DEFAULT_PASSWORD_POLICY: Lazy<Argon2> = Lazy::new(Argon2::default);
impl ClientMap {
pub fn new() -> ClientMap {
ClientMap::default()
}
pub fn register_client(&mut self, client: Client) {
let password_policy = Self::current_policy(&self.password_policy);
self.clients
.insert(client.client_id.clone(), client.encode(password_policy));
}
pub fn set_password_policy<P: PasswordPolicy + 'static>(&mut self, new_policy: P) {
self.password_policy = Some(Box::new(new_policy))
}
fn current_policy<'a>(policy: &'a Option<Box<dyn PasswordPolicy>>) -> &'a dyn PasswordPolicy {
policy
.as_ref()
.map(|boxed| &**boxed)
.unwrap_or(&*DEFAULT_PASSWORD_POLICY)
}
}
impl Extend<Client> for ClientMap {
fn extend<I>(&mut self, iter: I)
where
I: IntoIterator<Item = Client>,
{
iter.into_iter().for_each(|client| self.register_client(client))
}
}
impl FromIterator<Client> for ClientMap {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = Client>,
{
let mut into = ClientMap::new();
into.extend(iter);
into
}
}
impl<'s, R: Registrar + ?Sized> Registrar for &'s R {
fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result<BoundClient<'a>, RegistrarError> {
(**self).bound_redirect(bound)
}
fn negotiate(&self, bound: BoundClient, scope: Option<Scope>) -> Result<PreGrant, RegistrarError> {
(**self).negotiate(bound, scope)
}
fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> {
(**self).check(client_id, passphrase)
}
}
impl<'s, R: Registrar + ?Sized> Registrar for &'s mut R {
fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result<BoundClient<'a>, RegistrarError> {
(**self).bound_redirect(bound)
}
fn negotiate(&self, bound: BoundClient, scope: Option<Scope>) -> Result<PreGrant, RegistrarError> {
(**self).negotiate(bound, scope)
}
fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> {
(**self).check(client_id, passphrase)
}
}
impl<R: Registrar + ?Sized> Registrar for Box<R> {
fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result<BoundClient<'a>, RegistrarError> {
(**self).bound_redirect(bound)
}
fn negotiate(&self, bound: BoundClient, scope: Option<Scope>) -> Result<PreGrant, RegistrarError> {
(**self).negotiate(bound, scope)
}
fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> {
(**self).check(client_id, passphrase)
}
}
impl<R: Registrar + ?Sized> Registrar for Rc<R> {
fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result<BoundClient<'a>, RegistrarError> {
(**self).bound_redirect(bound)
}
fn negotiate(&self, bound: BoundClient, scope: Option<Scope>) -> Result<PreGrant, RegistrarError> {
(**self).negotiate(bound, scope)
}
fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> {
(**self).check(client_id, passphrase)
}
}
impl<R: Registrar + ?Sized> Registrar for Arc<R> {
fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result<BoundClient<'a>, RegistrarError> {
(**self).bound_redirect(bound)
}
fn negotiate(&self, bound: BoundClient, scope: Option<Scope>) -> Result<PreGrant, RegistrarError> {
(**self).negotiate(bound, scope)
}
fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> {
(**self).check(client_id, passphrase)
}
}
impl<'s, R: Registrar + ?Sized + 's> Registrar for MutexGuard<'s, R> {
fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result<BoundClient<'a>, RegistrarError> {
(**self).bound_redirect(bound)
}
fn negotiate(&self, bound: BoundClient, scope: Option<Scope>) -> Result<PreGrant, RegistrarError> {
(**self).negotiate(bound, scope)
}
fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> {
(**self).check(client_id, passphrase)
}
}
impl<'s, R: Registrar + ?Sized + 's> Registrar for RwLockWriteGuard<'s, R> {
fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result<BoundClient<'a>, RegistrarError> {
(**self).bound_redirect(bound)
}
fn negotiate(&self, bound: BoundClient, scope: Option<Scope>) -> Result<PreGrant, RegistrarError> {
(**self).negotiate(bound, scope)
}
fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> {
(**self).check(client_id, passphrase)
}
}
impl Registrar for ClientMap {
fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result<BoundClient<'a>, RegistrarError> {
let client = match self.clients.get(bound.client_id.as_ref()) {
None => return Err(RegistrarError::Unspecified),
Some(stored) => stored,
};
let registered_url = match bound.redirect_uri {
None => client.redirect_uri.clone(),
Some(url) => {
let original = std::iter::once(&client.redirect_uri);
let alternatives = client.additional_redirect_uris.iter();
if original
.chain(alternatives)
.any(|registered| *registered == *url.as_ref())
{
RegisteredUrl::Exact((*url).clone())
} else {
return Err(RegistrarError::Unspecified);
}
}
};
Ok(BoundClient {
client_id: bound.client_id,
redirect_uri: Cow::Owned(registered_url),
})
}
fn negotiate(&self, bound: BoundClient, _scope: Option<Scope>) -> Result<PreGrant, RegistrarError> {
let client = self
.clients
.get(bound.client_id.as_ref())
.expect("Bound client appears to not have been constructed with this registrar");
Ok(PreGrant {
client_id: bound.client_id.into_owned(),
redirect_uri: bound.redirect_uri.into_owned(),
scope: client.default_scope.clone(),
})
}
fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> {
let password_policy = Self::current_policy(&self.password_policy);
self.clients
.get(client_id)
.ok_or(RegistrarError::Unspecified)
.and_then(|client| {
RegisteredClient::new(client, password_policy).check_authentication(passphrase)
})?;
Ok(())
}
}
#[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::<Url>().unwrap().into(),
"default".parse().unwrap(),
);
register(registrar, public_client);
{
registrar
.check(public_id, None)
.expect("Authorization of public client has changed");
registrar
.check(public_id, Some(b""))
.err()
.expect("Authorization with password succeeded");
}
let private_client = Client::confidential(
private_id,
client_url.parse::<Url>().unwrap().into(),
"default".parse().unwrap(),
private_passphrase,
);
register(registrar, private_client);
{
registrar
.check(private_id, Some(private_passphrase))
.expect("Authorization with right password did not succeed");
registrar
.check(private_id, Some(b"Not the private passphrase"))
.err()
.expect("Authorization succeed with wrong password");
}
}
#[test]
fn public_client() {
let policy = Argon2::default();
let client = Client::public(
"ClientId",
"https://example.com".parse::<Url>().unwrap().into(),
"default".parse().unwrap(),
)
.encode(&policy);
let client = RegisteredClient::new(&client, &policy);
assert!(client.check_authentication(None).is_ok());
assert!(client.check_authentication(Some(b"")).is_err());
}
#[test]
fn confidential_client() {
let policy = Argon2::default();
let pass = b"AB3fAj6GJpdxmEVeNCyPoA==";
let client = Client::confidential(
"ClientId",
"https://example.com".parse::<Url>().unwrap().into(),
"default".parse().unwrap(),
pass,
)
.encode(&policy);
let client = RegisteredClient::new(&client, &policy);
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 with_additional_redirect_uris() {
let client_id = "ClientId";
let redirect_uri: Url = "https://example.com/foo".parse().unwrap();
let additional_redirect_uris: Vec<RegisteredUrl> =
vec!["https://example.com/bar".parse::<Url>().unwrap().into()];
let default_scope = "default".parse().unwrap();
let client = Client::public(client_id, redirect_uri.into(), default_scope)
.with_additional_redirect_uris(additional_redirect_uris);
let mut client_map = ClientMap::new();
client_map.register_client(client);
assert_eq!(
client_map
.bound_redirect(ClientUrl {
client_id: Cow::from(client_id),
redirect_uri: Some(Cow::Borrowed(&"https://example.com/foo".parse().unwrap()))
})
.unwrap()
.redirect_uri,
Cow::<Url>::Owned("https://example.com/foo".parse().unwrap())
);
assert_eq!(
client_map
.bound_redirect(ClientUrl {
client_id: Cow::from(client_id),
redirect_uri: Some(Cow::Borrowed(&"https://example.com/bar".parse().unwrap()))
})
.unwrap()
.redirect_uri,
Cow::<Url>::Owned("https://example.com/bar".parse().unwrap())
);
assert!(client_map
.bound_redirect(ClientUrl {
client_id: Cow::from(client_id),
redirect_uri: Some(Cow::Borrowed(&"https://example.com/baz".parse().unwrap()))
})
.is_err());
assert!(client_map
.bound_redirect(ClientUrl {
client_id: Cow::from(client_id),
redirect_uri: Some(Cow::Borrowed(&"https://example.com:1234/foo".parse().unwrap()))
})
.is_err());
}
#[test]
fn localhost_redirect_uris() {
let client_id = "ClientId";
let redirect_uri: Url = "http://localhost/foo".parse().unwrap();
let default_scope = "default".parse().unwrap();
let client = Client::public(
client_id,
RegisteredUrl::IgnorePortOnLocalhost(redirect_uri.into()),
default_scope,
);
let mut client_map = ClientMap::new();
client_map.register_client(client);
for url in &["http://localhost/foo", "http://localhost:1234/foo"] {
assert_eq!(
client_map
.bound_redirect(ClientUrl {
client_id: Cow::from(client_id),
redirect_uri: Some(Cow::Borrowed(&url.parse().unwrap()))
})
.unwrap()
.redirect_uri,
Cow::<Url>::Owned(url.parse().unwrap())
);
}
for url in &[
"http://localhost/bar",
"http://localhost:1234/bar",
"http://example.com/foo",
"http://example.com:1234/foo",
"http://127.0.0.1/foo",
"http://127.0.0.1:1234/foo",
] {
assert!(client_map
.bound_redirect(ClientUrl {
client_id: Cow::from(client_id),
redirect_uri: Some(Cow::Borrowed(&url.parse().unwrap()))
})
.is_err());
}
}
#[test]
fn client_map() {
let mut client_map = ClientMap::new();
simple_test_suite(&mut client_map, ClientMap::register_client);
}
#[test]
fn ignore_local_port_url_eq_local() {
let url = IgnoreLocalPortUrl::new("https://localhost/cb").unwrap();
let url2 = IgnoreLocalPortUrl::new("https://localhost:8000/cb").unwrap();
let aliases = [
"https://localhost/cb",
"https://localhost:1313/cb",
"https://localhost:8000/cb",
"https://localhost:8080/cb",
"https://localhost:4343/cb",
"https://localhost:08000/cb",
"https://localhost:0000008000/cb",
];
let others = [
"http://localhost/cb",
"https://localhost/cb/",
"https://127.0.0.1/cb",
"http://127.0.0.1/cb",
];
for alias in aliases.iter().map(|&a| IgnoreLocalPortUrl::new(a).unwrap()) {
assert_eq!(url, alias);
assert_eq!(url2, alias);
}
for other in others.iter().map(|&o| IgnoreLocalPortUrl::new(o).unwrap()) {
assert_ne!(url, other);
assert_ne!(url2, other);
}
}
#[test]
fn ignore_local_port_url_eq_not_local() {
let url1 = IgnoreLocalPortUrl::new("https://example.com/cb").unwrap();
let url2 = IgnoreLocalPortUrl::new("http://example.com/cb/").unwrap();
let not_url1 = ["https://example.com/cb/", "https://example.com:443/cb"];
let not_url2 = ["https://example.com/cb", "https://example.com:80/cb/"];
assert_eq!(
url1,
IgnoreLocalPortUrl(IgnoreLocalPortUrlInternal::Exact(
"https://example.com/cb".to_string()
))
);
assert_eq!(
url2,
IgnoreLocalPortUrl(IgnoreLocalPortUrlInternal::Exact(
"http://example.com/cb/".to_string()
))
);
for different_url in not_url1.iter().map(|&a| IgnoreLocalPortUrl::new(a).unwrap()) {
assert_ne!(url1, different_url);
}
for different_url in not_url2.iter().map(|&a| IgnoreLocalPortUrl::new(a).unwrap()) {
assert_ne!(url2, different_url);
}
}
#[test]
fn ignore_local_port_url_new() {
let locals = &[
"https://localhost/callback",
"https://localhost:8080/callback",
"http://localhost:8080/callback",
"https://localhost:8080",
];
let exacts = &[
"https://example.com:8000/callback",
"https://example.com:8080/callback",
"https://example.com:8888/callback",
"https://localhost.com:8888/callback",
];
for &local in locals {
let mut parsed: Url = local.parse().unwrap();
let _ = parsed.set_port(None);
assert_eq!(
local.parse(),
Ok(IgnoreLocalPortUrl(IgnoreLocalPortUrlInternal::Local(parsed)))
);
}
for &exact in exacts {
assert_eq!(
exact.parse(),
Ok(IgnoreLocalPortUrl(IgnoreLocalPortUrlInternal::Exact(
exact.into()
)))
);
}
}
#[test]
fn roundtrip_serialization_ignore_local_port_url() {
let url = "https://localhost/callback"
.parse::<IgnoreLocalPortUrl>()
.unwrap();
let serialized = rmp_serde::to_vec(&url).unwrap();
let deserialized = rmp_serde::from_slice::<IgnoreLocalPortUrl>(&serialized).unwrap();
assert_eq!(url, deserialized);
}
#[test]
fn deserialize_invalid_exact_url() {
let url = "/callback";
let serialized = rmp_serde::to_vec(&url).unwrap();
let deserialized = rmp_serde::from_slice::<ExactUrl>(&serialized);
assert!(deserialized.is_err());
}
#[test]
fn roundtrip_serialization_exact_url() {
let url = "https://example.com/callback".parse::<ExactUrl>().unwrap();
let serialized = rmp_serde::to_vec(&url).unwrap();
let deserialized = rmp_serde::from_slice::<ExactUrl>(&serialized).unwrap();
assert_eq!(url, deserialized);
}
}