use crate::errors::{Error, RequestError};
use http::{header::CONTENT_TYPE, Request, Uri};
use std::convert::TryInto;
use url::form_urlencoded::Serializer;
#[derive(serde::Deserialize, Debug)]
struct TokenExchangeResponse {
access_token: String,
token_type: String,
expires_in: i64,
scope: String,
id_token: Option<String>,
refresh_token: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Token {
pub access_token: String,
pub token_type: String,
pub expires_in: i64,
pub expires_in_timestamp: i64,
pub scope: String,
pub id_token: Option<String>,
pub refresh_token: Option<String>,
}
impl Token {
pub fn from_response<S>(response: http::Response<S>) -> Result<Self, Error>
where
S: AsRef<[u8]>,
{
parse_token_response(response)
}
}
impl From<TokenExchangeResponse> for Token {
fn from(t: TokenExchangeResponse) -> Token {
let expires_ts = chrono::Utc::now().timestamp() + t.expires_in;
Token {
access_token: t.access_token,
token_type: t.token_type,
refresh_token: t.refresh_token,
expires_in: t.expires_in,
expires_in_timestamp: expires_ts,
scope: t.scope,
id_token: t.id_token,
}
}
}
pub fn exchange_token_request<ReqUri, RedirectUri>(
uri: ReqUri,
redirect_uri: RedirectUri,
client_id: &str,
auth_code: &str,
client_secret: Option<&str>,
code_verifier: Option<&str>,
) -> Result<Request<Vec<u8>>, RequestError>
where
ReqUri: TryInto<Uri>,
RedirectUri: TryInto<Uri>,
{
let mut serializer = Serializer::new(String::new());
serializer.append_pair("client_id", client_id);
serializer.append_pair("redirect_uri", &into_uri(redirect_uri)?.to_string());
serializer.append_pair("grant_type", "authorization_code");
serializer.append_pair("code", auth_code);
if let Some(cs) = client_secret {
serializer.append_pair("client_secret", cs);
}
if let Some(cv) = code_verifier {
serializer.append_pair("code_verifier", cv);
}
let body = serializer.finish();
http_post_req(body, uri)
}
pub(crate) fn into_uri<U: TryInto<Uri>>(uri: U) -> Result<Uri, RequestError> {
uri.try_into().map_err(|_err| RequestError::InvalidUri)
}
pub fn parse_token_response<S>(response: http::Response<S>) -> Result<Token, Error>
where
S: AsRef<[u8]>,
{
let (parts, body) = response.into_parts();
if !parts.status.is_success() {
return Err(Error::HttpStatus(parts.status));
}
let token_res: TokenExchangeResponse = serde_json::from_slice(body.as_ref())?;
let token: Token = token_res.into();
Ok(token)
}
pub fn refresh_token_request<ReqUri>(
uri: ReqUri,
client_id: &str,
client_secret: &str,
refresh_token: &str,
) -> Result<Request<Vec<u8>>, RequestError>
where
ReqUri: TryInto<Uri>,
{
let body = Serializer::new(String::new())
.append_pair("client_id", client_id)
.append_pair("client_secret", client_secret)
.append_pair("grant_type", "refresh_token")
.append_pair("refresh_token", refresh_token)
.finish();
http_post_req(body, uri)
}
fn http_post_req<ReqUri>(body: String, uri: ReqUri) -> Result<Request<Vec<u8>>, RequestError>
where
ReqUri: TryInto<Uri>,
{
let req_body = Vec::from(body);
Ok(Request::builder()
.method("POST")
.uri(into_uri(uri)?)
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(req_body)?)
}
#[cfg(test)]
mod test {
use super::*;
use std::str;
#[test]
fn pkce_flow_exchange() {
let request = exchange_token_request(
"https://www.example.com/",
"http://localhost:8000/",
"client_id",
"auth-code",
None,
Some("the_secret_code_verifier"),
)
.unwrap();
let body = str::from_utf8(request.body()).unwrap();
assert!(!body.contains("client_secret"));
assert!(body.contains("code_verifier=the_secret_code_verifier"));
}
}