use crate::error::{Error, Result};
use crate::util::ResponseExt;
use serde::Deserialize;
#[derive(Clone, Debug, Default)]
pub struct Permission {
write: bool,
access_shared: bool,
offline_access: bool,
}
impl Permission {
pub fn new_read() -> Self {
Self::default()
}
pub fn write(mut self, write: bool) -> Self {
self.write = write;
self
}
pub fn access_shared(mut self, access_shared: bool) -> Self {
self.access_shared = access_shared;
self
}
pub fn offline_access(mut self, offline_access: bool) -> Self {
self.offline_access = offline_access;
self
}
fn to_scope_str(&self) -> &'static str {
macro_rules! cond_concat {
($($s:literal,)*) => { concat!($($s),*) };
($($s:literal,)* ($cond:expr, $t:literal, $f:literal), $($tt:tt)*) => {
if $cond { cond_concat!($($s,)* $t, $($tt)*) }
else { cond_concat!($($s,)* $f, $($tt)*) }
};
}
cond_concat![
(self.offline_access, "offline_access ", ""),
(self.write, "files.readwrite", "files.read"),
(self.access_shared, ".all", ""),
]
}
}
#[derive(Debug)]
pub struct AuthClient {
client: ::reqwest::Client,
client_id: String,
permission: Permission,
redirect_uri: String,
}
impl AuthClient {
pub fn new(client_id: String, permission: Permission, redirect_uri: String) -> Self {
AuthClient {
client: ::reqwest::Client::new(),
client_id,
permission,
redirect_uri,
}
}
fn get_auth_url(&self, response_type: &str) -> String {
::url::Url::parse_with_params(
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
&[
("client_id", &self.client_id as &str),
("scope", self.permission.to_scope_str()),
("redirect_uri", &self.redirect_uri),
("response_type", response_type),
],
)
.unwrap()
.into_string()
}
pub fn get_token_auth_url(&self) -> String {
self.get_auth_url("token")
}
pub fn get_code_auth_url(&self) -> String {
self.get_auth_url("code")
}
fn request_authorize(&self, require_refresh: bool, params: &[(&str, &str)]) -> Result<Token> {
#[derive(Deserialize)]
struct Response {
access_token: String,
refresh_token: Option<String>,
}
let resp: Response = self
.client
.post("https://login.microsoftonline.com/common/oauth2/v2.0/token")
.form(params)
.send()?
.parse()?;
if require_refresh && resp.refresh_token.is_none() {
return Err(Error::unexpected_response("Missing field `refresh_token`"));
}
Ok(Token {
token: resp.access_token,
refresh_token: resp.refresh_token,
_private: (),
})
}
pub fn login_with_code(&self, code: &str, client_secret: Option<&str>) -> Result<Token> {
self.request_authorize(
self.permission.offline_access,
&[
("client_id", &self.client_id as &str),
("client_secret", client_secret.unwrap_or("")),
("code", code),
("grant_type", "authorization_code"),
("redirect_uri", &self.redirect_uri),
],
)
}
pub fn login_with_refresh_token(
&self,
refresh_token: &str,
client_secret: Option<&str>,
) -> Result<Token> {
assert!(
self.permission.offline_access,
"Refresh token requires offline_access permission."
);
self.request_authorize(
true,
&[
("client_id", &self.client_id as &str),
("client_secret", client_secret.unwrap_or("")),
("grant_type", "refresh_token"),
("redirect_uri", &self.redirect_uri),
("refresh_token", refresh_token),
],
)
}
}
#[derive(Debug)]
pub struct Token {
pub token: String,
pub refresh_token: Option<String>,
_private: (),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scope_string() {
for &write in &[false, true] {
for &shared in &[false, true] {
for &offline in &[false, true] {
assert_eq!(
Permission::new_read()
.write(write)
.access_shared(shared)
.offline_access(offline)
.to_scope_str(),
format!(
"{}{}{}",
if offline { "offline_access " } else { "" },
if write {
"files.readwrite"
} else {
"files.read"
},
if shared { ".all" } else { "" },
),
"When testing write={}, shared={}, offline={}",
write,
shared,
offline,
);
}
}
}
}
}