use sofie::core::{Error, Result};
use sofie::values::Secret;
use super::keys::{Keys, KeysAt};
pub(crate) const TENANT_ID: &str = "{tenantid}";
#[derive(Debug)]
pub(crate) enum Claims {
IdToken { issuer: String, keys: Keys },
UserEndpoints { user: String, emails: String },
}
#[derive(Debug)]
pub struct Provider {
pub(crate) name: String,
pub(crate) label: String,
pub(crate) authorization_url: String,
pub(crate) token_url: String,
pub(crate) claims: Claims,
pub(crate) client_id: String,
pub(crate) client_secret: Secret<String>,
pub(crate) scopes: String,
}
impl Provider {
pub fn openid(
name: &str,
label: &str,
issuer: &str,
authorization_url: &str,
token_url: &str,
client: (String, Secret<String>),
) -> Result<Self> {
if name.is_empty()
|| !name
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte == b'-')
{
return Err(Error::internal_message(format!(
"an OAuth provider's name is lowercase letters and `-`, not `{name}`"
)));
}
let (client_id, client_secret) = client;
Ok(Self {
name: name.to_owned(),
label: label.to_owned(),
authorization_url: authorization_url.to_owned(),
token_url: token_url.to_owned(),
claims: Claims::IdToken {
issuer: issuer.to_owned(),
keys: Keys::new(KeysAt::Discovery),
},
client_id,
client_secret,
scopes: "openid email".to_owned(),
})
}
pub fn google(client: (String, Secret<String>)) -> Result<Self> {
Self::openid(
"google",
"Google",
"https://accounts.google.com",
"https://accounts.google.com/o/oauth2/v2/auth",
"https://oauth2.googleapis.com/token",
client,
)
.map(|google| google.keys_at("https://www.googleapis.com/oauth2/v3/certs"))
}
pub fn microsoft(tenant: &str, client: (String, Secret<String>)) -> Result<Self> {
Self::openid(
"microsoft",
"Microsoft",
&format!("https://login.microsoftonline.com/{TENANT_ID}/v2.0"),
&format!("https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize"),
&format!("https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"),
client,
)
.map(|microsoft| {
microsoft.keys_at(&format!(
"https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys"
))
})
}
fn keys_at(mut self, url: &str) -> Self {
if let Claims::IdToken { keys, .. } = &mut self.claims {
*keys = Keys::new(KeysAt::Url(url.to_owned()));
}
self
}
pub fn github(client: (String, Secret<String>)) -> Result<Self> {
let (client_id, client_secret) = client;
Ok(Self {
name: "github".to_owned(),
label: "GitHub".to_owned(),
authorization_url: "https://github.com/login/oauth/authorize".to_owned(),
token_url: "https://github.com/login/oauth/access_token".to_owned(),
claims: Claims::UserEndpoints {
user: "https://api.github.com/user".to_owned(),
emails: "https://api.github.com/user/emails".to_owned(),
},
client_id,
client_secret,
scopes: "read:user user:email".to_owned(),
})
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
pub(crate) fn requested_urls(&self) -> Vec<&str> {
let mut urls = vec![self.token_url.as_str()];
match &self.claims {
Claims::IdToken { issuer, keys } => urls.push(keys.url().unwrap_or(issuer)),
Claims::UserEndpoints { user, emails } => urls.extend([user.as_str(), emails.as_str()]),
}
urls
}
}