use crate::auth_scheme::{AuthenticationScheme, ClientAuthentication};
use crate::errors::{Error, RequestError};
use data_encoding::BASE64;
use http::header::AUTHORIZATION;
use http::{header::CONTENT_TYPE, Request, Uri};
use std::convert::TryInto;
use std::time::{SystemTime, UNIX_EPOCH};
use url::form_urlencoded::Serializer;
pub fn authorization_request<ReqUri, RedirectUri>(
uri: ReqUri,
redirect_uri: RedirectUri,
auth: &ClientAuthentication,
scopes: &Option<Vec<String>>,
) -> Result<Request<Vec<u8>>, RequestError>
where
ReqUri: TryInto<Uri>,
RedirectUri: TryInto<Uri>,
{
let request_builder = Request::builder()
.method("POST")
.uri(into_uri(uri)?)
.header(CONTENT_TYPE, "application/x-www-form-urlencoded");
let mut serializer = Serializer::new(String::new());
serializer.append_pair("redirect_uri", &into_uri(redirect_uri)?.to_string());
serializer.append_pair("grant_type", "authorization_code");
serializer.append_pair("response_type", "code");
serializer.append_pair(
"scope",
&scopes
.as_ref()
.map_or_else(|| "openid".to_owned(), |s| s.join(" ")),
);
if let Some(state) = &auth.state {
serializer.append_pair("state", state);
}
if let Some(nonce) = &auth.nonce {
serializer.append_pair("nonce", nonce);
}
serializer.append_pair("client_id", &auth.client_id);
let body = match &auth.scheme {
AuthenticationScheme::Basic(client_credentials) => {
let basic = BASE64.encode(
format!("{}:{}", &auth.client_id, client_credentials.client_secret).as_bytes(),
);
request_builder
.header(AUTHORIZATION, format!("Basic {}", basic))
.body(Vec::from(serializer.finish()))
}
AuthenticationScheme::Post(client_credentials) => request_builder.body(Vec::from(
serializer
.append_pair("client_secret", &client_credentials.client_secret)
.finish(),
)),
AuthenticationScheme::Pkce(pkce) => request_builder.body(Vec::from(
serializer
.append_pair("code_challenge", &pkce.code_challenge)
.append_pair("code_challenge_method", &pkce.code_challenge_method)
.finish(),
)),
}?;
Ok(body)
}
#[derive(serde::Deserialize, Debug)]
struct TokenExchangeResponse {
access_token: String,
token_type: String,
expires_in: Option<i64>,
scope: String,
id_token: Option<String>,
refresh_token: Option<String>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Token {
pub access_token: String,
pub token_type: String,
pub expires_in: Option<i64>,
pub expires_in_timestamp: Option<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 = t.expires_in.and_then(|time_until| {
SystemTime::now()
.duration_since(UNIX_EPOCH) .ok()
.and_then(|time_stamp| time_stamp.as_secs().try_into().ok()) .map(|now_as_seconds: i64| time_until + now_as_seconds)
});
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,
auth: &ClientAuthentication,
auth_code: &str,
) -> Result<Request<Vec<u8>>, RequestError>
where
ReqUri: TryInto<Uri>,
RedirectUri: TryInto<Uri>,
{
let mut serializer = Serializer::new(String::new());
serializer.append_pair("redirect_uri", &into_uri(redirect_uri)?.to_string());
serializer.append_pair("grant_type", "authorization_code");
serializer.append_pair("code", auth_code);
let request_builder = Request::builder()
.method("POST")
.uri(into_uri(uri)?)
.header(CONTENT_TYPE, "application/x-www-form-urlencoded");
serializer.append_pair("client_id", &auth.client_id);
let body = match &auth.scheme {
AuthenticationScheme::Basic(client_credentials) => {
let basic = BASE64.encode(
format!("{}:{}", &auth.client_id, client_credentials.client_secret).as_bytes(),
);
request_builder
.header(AUTHORIZATION, format!("Basic {}", basic))
.body(Vec::from(serializer.finish()))
}
AuthenticationScheme::Post(client_credentials) => request_builder.body(Vec::from(
serializer
.append_pair("client_secret", &client_credentials.client_secret)
.finish(),
)),
AuthenticationScheme::Pkce(pkce) => {
if let Some(client_secret) = &pkce.client_secret {
serializer.append_pair("client_secret", client_secret);
}
request_builder.body(Vec::from(
serializer
.append_pair("code_verifier", &pkce.code_verifier)
.finish(),
))
}
}?;
Ok(body)
}
pub(crate) fn into_uri<U: TryInto<Uri>>(uri: U) -> Result<Uri, RequestError> {
uri.try_into().map_err(|_err| RequestError::InvalidUri)
}
pub(crate) fn user_info_request<ReqUri>(
uri: ReqUri,
access_token: &str,
) -> Result<Request<Vec<u8>>, RequestError>
where
ReqUri: TryInto<Uri>,
{
Ok(Request::get(into_uri(uri)?)
.header(AUTHORIZATION, format!("Bearer {}", access_token))
.body(vec![])?)
}
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() {
println!("{:?}", core::str::from_utf8(body.as_ref()));
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,
auth: &ClientAuthentication,
refresh_token: &str,
) -> Result<Request<Vec<u8>>, RequestError>
where
ReqUri: TryInto<Uri>,
{
let mut partial = Serializer::new(String::new());
partial.append_pair("grant_type", "refresh_token");
partial.append_pair("refresh_token", refresh_token);
let request_builder = Request::builder()
.method("POST")
.uri(into_uri(uri)?)
.header(CONTENT_TYPE, "application/x-www-form-urlencoded");
partial.append_pair("client_id", &auth.client_id);
let body = match &auth.scheme {
AuthenticationScheme::Basic(client_credentials) => {
let basic = BASE64.encode(
format!("{}:{}", &auth.client_id, client_credentials.client_secret).as_bytes(),
);
request_builder
.header(AUTHORIZATION, format!("Basic {}", basic))
.body(Vec::from(partial.finish()))
}
AuthenticationScheme::Post(client_credentials) => request_builder.body(Vec::from(
partial
.append_pair("client_secret", &client_credentials.client_secret)
.finish(),
)),
AuthenticationScheme::Pkce(pkce) => {
if let Some(client_secret) = &pkce.client_secret {
partial.append_pair("client_secret", client_secret);
}
request_builder.body(Vec::from(
partial
.append_pair("code_verifier", &pkce.code_verifier)
.finish(),
))
}
}?;
Ok(body)
}
#[cfg(test)]
mod test {
use super::*;
use crate::auth_scheme::PkceCredentials;
use std::str;
#[test]
fn pkce_flow_exchange() {
let spooky_secret_verifier = "the_secret_verifier".to_owned();
let client_credentials = ClientAuthentication::new(
"client_id".to_owned(),
AuthenticationScheme::Pkce(PkceCredentials::new(
"ch".to_owned(),
"&S256".to_owned(),
spooky_secret_verifier,
None,
)),
None,
None,
);
let request = exchange_token_request(
"https://www.example.com/",
"http://localhost:8000/",
&client_credentials,
"auth-code",
)
.unwrap();
let body = str::from_utf8(request.body()).unwrap();
assert!(!body.contains("client_secret"));
assert!(body.contains("code_verifier"));
}
}