#![deny(missing_docs)]
use std::{borrow::Cow, error, fmt, time::Duration};
use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
pub use url::Url;
#[derive(Clone, Copy, Debug)]
pub enum AuthType {
RequestBody,
BasicAuth,
}
macro_rules! redacted_debug {
($name:ident) => {
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, concat!(stringify!($name), "([redacted])"))
}
}
};
}
macro_rules! borrowed_newtype {
($name:ident, $borrowed:ty) => {
impl std::ops::Deref for $name {
type Target = $borrowed;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> Into<Cow<'a, $borrowed>> for &'a $name {
fn into(self) -> Cow<'a, $borrowed> {
Cow::Borrowed(&self.0)
}
}
impl AsRef<$borrowed> for $name {
fn as_ref(&self) -> &$borrowed {
self
}
}
};
}
macro_rules! newtype {
($name:ident, $owned:ty, $borrowed:ty) => {
borrowed_newtype!($name, $borrowed);
impl<'a> From<&'a $borrowed> for $name {
fn from(value: &'a $borrowed) -> Self {
Self(value.to_owned())
}
}
impl From<$owned> for $name {
fn from(value: $owned) -> Self {
Self(value)
}
}
impl<'a> From<&'a $owned> for $name {
fn from(value: &'a $owned) -> Self {
Self(value.to_owned())
}
}
impl<'a> Into<$owned> for $name {
fn into(self) -> $owned {
self.0
}
}
};
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub struct Scope(String);
newtype!(Scope, String, str);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub struct PkceCodeChallengeS256(String);
newtype!(PkceCodeChallengeS256, String, str);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub struct PkceCodeChallengeMethod(String);
newtype!(PkceCodeChallengeMethod, String, str);
#[derive(Clone, Deserialize, Serialize)]
pub struct ClientSecret(String);
redacted_debug!(ClientSecret);
newtype!(ClientSecret, String, str);
#[must_use]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct State([u8; 16]);
redacted_debug!(State);
borrowed_newtype!(State, [u8]);
impl State {
pub fn new_random() -> Self {
let mut random_bytes = [0u8; 16];
thread_rng().fill(&mut random_bytes);
State(random_bytes)
}
pub fn to_base64(&self) -> String {
base64::encode_config(&self.0, base64::URL_SAFE_NO_PAD)
}
}
impl serde::Serialize for State {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_base64().serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for State {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let bytes =
base64::decode_config(&s, base64::URL_SAFE_NO_PAD).map_err(serde::de::Error::custom)?;
let mut buf = [0u8; 16];
buf.copy_from_slice(&bytes);
Ok(Self(buf))
}
}
#[derive(Deserialize, Serialize)]
pub struct PkceCodeVerifierS256(String);
newtype!(PkceCodeVerifierS256, String, str);
impl PkceCodeVerifierS256 {
pub fn new_random() -> Self {
PkceCodeVerifierS256::new_random_len(32)
}
pub fn new_random_len(num_bytes: u32) -> Self {
assert!(num_bytes >= 32 && num_bytes <= 96);
let random_bytes: Vec<u8> = (0..num_bytes).map(|_| thread_rng().gen::<u8>()).collect();
let code = base64::encode_config(&random_bytes, base64::URL_SAFE_NO_PAD);
assert!(code.len() >= 43 && code.len() <= 128);
PkceCodeVerifierS256(code)
}
pub fn code_challenge(&self) -> PkceCodeChallengeS256 {
let digest = Sha256::digest(self.as_bytes());
PkceCodeChallengeS256::from(base64::encode_config(&digest, base64::URL_SAFE_NO_PAD))
}
pub fn code_challenge_method() -> PkceCodeChallengeMethod {
PkceCodeChallengeMethod::from("S256".to_string())
}
pub fn authorize_url_params(&self) -> Vec<(&'static str, String)> {
vec![
(
"code_challenge_method",
PkceCodeVerifierS256::code_challenge_method().into(),
),
("code_challenge", self.code_challenge().into()),
]
}
}
#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AuthorizationCode(String);
redacted_debug!(AuthorizationCode);
newtype!(AuthorizationCode, String, str);
#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RefreshToken(String);
redacted_debug!(RefreshToken);
newtype!(RefreshToken, String, str);
#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AccessToken(String);
redacted_debug!(AccessToken);
newtype!(AccessToken, String, str);
pub struct ResourceOwnerPassword(String);
newtype!(ResourceOwnerPassword, String, str);
#[derive(Clone, Debug)]
pub struct Client {
client_id: String,
client_secret: Option<ClientSecret>,
auth_url: Url,
auth_type: AuthType,
token_url: Url,
scopes: Vec<Scope>,
redirect_url: Option<Url>,
}
impl Client {
pub fn new(client_id: impl AsRef<str>, auth_url: Url, token_url: Url) -> Self {
Client {
client_id: client_id.as_ref().to_string(),
client_secret: None,
auth_url,
auth_type: AuthType::BasicAuth,
token_url,
scopes: Vec::new(),
redirect_url: None,
}
}
pub fn set_client_secret(&mut self, client_secret: impl Into<ClientSecret>) {
self.client_secret = Some(client_secret.into());
}
pub fn add_scope(&mut self, scope: impl Into<Scope>) {
self.scopes.push(scope.into());
}
pub fn set_auth_type(&mut self, auth_type: AuthType) {
self.auth_type = auth_type;
}
pub fn set_redirect_url(&mut self, redirect_url: Url) {
self.redirect_url = Some(redirect_url);
}
pub fn authorize_url(&self, state: &State) -> Url {
self.authorize_url_impl("code", state)
}
pub fn authorize_url_implicit(&self, state: &State) -> Url {
self.authorize_url_impl("token", state)
}
fn authorize_url_impl(&self, response_type: &str, state: &State) -> Url {
let scopes = self
.scopes
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join(" ");
let mut url = self.auth_url.clone();
{
let mut query = url.query_pairs_mut();
query.append_pair("response_type", response_type);
query.append_pair("client_id", &self.client_id);
if let Some(ref redirect_url) = self.redirect_url {
query.append_pair("redirect_uri", redirect_url.as_str());
}
if !scopes.is_empty() {
query.append_pair("scope", &scopes);
}
query.append_pair("state", &state.to_base64());
}
url
}
pub fn exchange_code(&self, code: impl Into<AuthorizationCode>) -> Request<'_> {
let code = code.into();
self.request_token()
.param("grant_type", "authorization_code")
.param("code", code.to_string())
}
pub fn exchange_password<'a>(
&'a self,
username: impl AsRef<str>,
password: impl AsRef<str>,
) -> Request<'a> {
let username = username.as_ref();
let password = password.as_ref();
let mut builder = self
.request_token()
.param("grant_type", "password")
.param("username", username.to_string())
.param("password", password.to_string());
if !self.scopes.is_empty() {
let scopes = self
.scopes
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join(" ");
builder = builder.param("scope", scopes);
}
builder
}
pub fn exchange_client_credentials(&self) -> Request<'_> {
let mut builder = self
.request_token()
.param("grant_type", "client_credentials");
if !self.scopes.is_empty() {
let scopes = self
.scopes
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join(" ");
builder = builder.param("scopes", scopes);
}
builder
}
pub fn exchange_refresh_token(&self, refresh_token: &RefreshToken) -> Request<'_> {
self.request_token()
.param("grant_type", "refresh_token")
.param("refresh_token", refresh_token.to_string())
}
fn request_token(&self) -> Request<'_> {
Request {
token_url: &self.token_url,
auth_type: self.auth_type,
client_id: &self.client_id,
client_secret: self.client_secret.as_ref(),
redirect_url: self.redirect_url.as_ref(),
params: vec![],
}
}
}
pub struct ClientRequest<'a, 'client> {
request: Request<'a>,
client: &'client reqwest::Client,
}
impl<'a, 'b> ClientRequest<'a, 'b> {
pub async fn execute<T>(self) -> Result<T, RequestTokenError>
where
T: Token,
{
use reqwest::{header, Method};
let token_url = self.request.token_url;
let mut request = self.client.request(Method::POST, &token_url.to_string());
request = request.header(
header::ACCEPT,
header::HeaderValue::from_static(CONTENT_TYPE_JSON),
);
let request = {
let mut form = url::form_urlencoded::Serializer::new(String::new());
match self.request.auth_type {
AuthType::RequestBody => {
form.append_pair("client_id", self.request.client_id);
if let Some(client_secret) = self.request.client_secret {
form.append_pair("client_secret", client_secret);
}
}
AuthType::BasicAuth => {
let username = url_encode(self.request.client_id);
let password = match self.request.client_secret {
Some(client_secret) => Some(url_encode(client_secret)),
None => None,
};
request = request.basic_auth(&username, password.as_ref());
}
}
for (key, value) in self.request.params {
form.append_pair(key.as_ref(), value.as_ref());
}
if let Some(ref redirect_url) = self.request.redirect_url {
form.append_pair("redirect_uri", redirect_url.as_str());
}
request = request.header(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/x-www-form-urlencoded"),
);
request.body(form.finish().into_bytes())
};
let res = request.send().await.map_err(RequestTokenError::Client)?;
let status = res.status();
let body = res.bytes().await.map_err(RequestTokenError::Client)?;
if !status.is_success() {
if body.is_empty() {
return Err(RequestTokenError::Other(
"Server returned empty error response".into(),
));
} else {
println!("body: {:?}", body);
let error = match serde_json::from_slice::<ErrorResponse>(body.as_ref()) {
Ok(error) => RequestTokenError::ServerResponse(error),
Err(error) => RequestTokenError::Parse(error, body.as_ref().to_vec()),
};
return Err(error);
}
}
if body.is_empty() {
return Err(RequestTokenError::Other(
"Server returned empty response body".into(),
));
}
return serde_json::from_slice(body.as_ref())
.map_err(|e| RequestTokenError::Parse(e, body.as_ref().to_vec()));
fn url_encode(s: &str) -> String {
url::form_urlencoded::byte_serialize(s.as_bytes()).collect::<String>()
}
const CONTENT_TYPE_JSON: &str = "application/json";
}
}
pub struct Request<'a> {
token_url: &'a Url,
auth_type: AuthType,
client_id: &'a str,
client_secret: Option<&'a ClientSecret>,
redirect_url: Option<&'a Url>,
params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
}
impl<'a> Request<'a> {
pub fn param(mut self, key: impl Into<Cow<'a, str>>, value: impl Into<Cow<'a, str>>) -> Self {
self.params.push((key.into(), value.into()));
self
}
pub fn with_client<'client>(
self,
client: &'client reqwest::Client,
) -> ClientRequest<'a, 'client> {
ClientRequest {
client,
request: self,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum TokenType {
Bearer,
Mac,
}
impl<'de> serde::de::Deserialize<'de> for TokenType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?.to_lowercase();
return match value.as_str() {
"bearer" => Ok(TokenType::Bearer),
"mac" => Ok(TokenType::Mac),
other => Err(serde::de::Error::custom(UnknownVariantError(
other.to_string(),
))),
};
#[derive(Debug)]
struct UnknownVariantError(String);
impl error::Error for UnknownVariantError {}
impl fmt::Display for UnknownVariantError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "unsupported variant: {}", self.0)
}
}
}
}
pub trait Token
where
Self: for<'a> serde::de::Deserialize<'a>,
{
fn access_token(&self) -> &AccessToken;
fn token_type(&self) -> &TokenType;
fn expires_in(&self) -> Option<Duration>;
fn refresh_token(&self) -> Option<&RefreshToken>;
fn scopes(&self) -> Option<&Vec<Scope>>;
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct StandardToken {
access_token: AccessToken,
token_type: TokenType,
#[serde(skip_serializing_if = "Option::is_none")]
expires_in: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
refresh_token: Option<RefreshToken>,
#[serde(rename = "scope")]
#[serde(deserialize_with = "helpers::deserialize_space_delimited_vec")]
#[serde(serialize_with = "helpers::serialize_space_delimited_vec")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
scopes: Option<Vec<Scope>>,
}
impl Token for StandardToken {
fn access_token(&self) -> &AccessToken {
&self.access_token
}
fn token_type(&self) -> &TokenType {
&self.token_type
}
fn expires_in(&self) -> Option<Duration> {
self.expires_in.map(Duration::from_secs)
}
fn refresh_token(&self) -> Option<&RefreshToken> {
self.refresh_token.as_ref()
}
fn scopes(&self) -> Option<&Vec<Scope>> {
self.scopes.as_ref()
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorField {
InvalidRequest,
InvalidClient,
InvalidGrant,
UnauthorizedClient,
UnsupportedGrantType,
InvalidScope,
Other(String),
}
impl fmt::Display for ErrorField {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::ErrorField::*;
match *self {
InvalidRequest => "invalid_request".fmt(fmt),
InvalidClient => "invalid_client".fmt(fmt),
InvalidGrant => "invalid_grant".fmt(fmt),
UnauthorizedClient => "unauthorized_client".fmt(fmt),
UnsupportedGrantType => "unsupported_grant_type".fmt(fmt),
InvalidScope => "invalid_scope".fmt(fmt),
Other(ref value) => value.fmt(fmt),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ErrorResponse {
pub error: ErrorField,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub error_description: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub error_uri: Option<String>,
}
impl fmt::Display for ErrorResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut formatted = self.error.to_string();
if let Some(error_description) = self.error_description.as_ref() {
formatted.push_str(": ");
formatted.push_str(error_description);
}
if let Some(error_uri) = self.error_uri.as_ref() {
formatted.push_str(" / See ");
formatted.push_str(error_uri);
}
write!(f, "{}", formatted)
}
}
impl error::Error for ErrorResponse {}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum NewClientError {
#[error("Failed to construct client")]
Reqwest(#[source] reqwest::Error),
}
impl From<reqwest::Error> for NewClientError {
fn from(error: reqwest::Error) -> Self {
Self::Reqwest(error)
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum RequestTokenError {
#[error("Server returned error response")]
ServerResponse(#[source] ErrorResponse),
#[error("Client error")]
Client(#[source] reqwest::Error),
#[error("Failed to parse server response")]
Parse(#[source] serde_json::error::Error, Vec<u8>),
#[error("Other error: {0}")]
Other(Cow<'static, str>),
}
pub mod helpers {
use serde::{Deserialize, Deserializer, Serializer};
use url::Url;
pub fn deserialize_space_delimited_vec<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: Default + Deserialize<'de>,
D: Deserializer<'de>,
{
use serde::de::Error;
use serde_json::Value;
if let Some(space_delimited) = Option::<String>::deserialize(deserializer)? {
let entries = space_delimited
.split(' ')
.map(|s| Value::String(s.to_string()))
.collect();
return T::deserialize(Value::Array(entries)).map_err(Error::custom);
}
Ok(T::default())
}
pub fn serialize_space_delimited_vec<T, S>(
vec_opt: &Option<Vec<T>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
T: AsRef<str>,
S: Serializer,
{
if let Some(ref vec) = *vec_opt {
let space_delimited = vec.iter().map(|s| s.as_ref()).collect::<Vec<_>>().join(" ");
serializer.serialize_str(&space_delimited)
} else {
serializer.serialize_none()
}
}
pub fn deserialize_url<'de, D>(deserializer: D) -> Result<Url, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let url_str = String::deserialize(deserializer)?;
Url::parse(url_str.as_ref()).map_err(Error::custom)
}
pub fn serialize_url<S>(url: &Url, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(url.as_str())
}
}