sofie_identity/oauth/
provider.rs1use sofie::core::{Error, Result};
2use sofie::values::Secret;
3
4use super::keys::{Keys, KeysAt};
5
6pub(crate) const TENANT_ID: &str = "{tenantid}";
9
10#[derive(Debug)]
11pub(crate) enum Claims {
12 IdToken { issuer: String, keys: Keys },
14 UserEndpoints { user: String, emails: String },
16}
17
18#[derive(Debug)]
20pub struct Provider {
21 pub(crate) name: String,
22 pub(crate) label: String,
23 pub(crate) authorization_url: String,
24 pub(crate) token_url: String,
25 pub(crate) claims: Claims,
26 pub(crate) client_id: String,
27 pub(crate) client_secret: Secret<String>,
28 pub(crate) scopes: String,
29}
30
31impl Provider {
32 pub fn openid(
33 name: &str,
34 label: &str,
35 issuer: &str,
36 authorization_url: &str,
37 token_url: &str,
38 client: (String, Secret<String>),
39 ) -> Result<Self> {
40 if name.is_empty()
41 || !name
42 .bytes()
43 .all(|byte| byte.is_ascii_lowercase() || byte == b'-')
44 {
45 return Err(Error::internal_message(format!(
46 "an OAuth provider's name is lowercase letters and `-`, not `{name}`"
47 )));
48 }
49 let (client_id, client_secret) = client;
50 Ok(Self {
51 name: name.to_owned(),
52 label: label.to_owned(),
53 authorization_url: authorization_url.to_owned(),
54 token_url: token_url.to_owned(),
55 claims: Claims::IdToken {
56 issuer: issuer.to_owned(),
57 keys: Keys::new(KeysAt::Discovery),
58 },
59 client_id,
60 client_secret,
61 scopes: "openid email".to_owned(),
62 })
63 }
64
65 pub fn google(client: (String, Secret<String>)) -> Result<Self> {
66 Self::openid(
67 "google",
68 "Google",
69 "https://accounts.google.com",
70 "https://accounts.google.com/o/oauth2/v2/auth",
71 "https://oauth2.googleapis.com/token",
72 client,
73 )
74 .map(|google| google.keys_at("https://www.googleapis.com/oauth2/v3/certs"))
75 }
76
77 pub fn microsoft(tenant: &str, client: (String, Secret<String>)) -> Result<Self> {
79 Self::openid(
80 "microsoft",
81 "Microsoft",
82 &format!("https://login.microsoftonline.com/{TENANT_ID}/v2.0"),
83 &format!("https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize"),
84 &format!("https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"),
85 client,
86 )
87 .map(|microsoft| {
88 microsoft.keys_at(&format!(
89 "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys"
90 ))
91 })
92 }
93
94 fn keys_at(mut self, url: &str) -> Self {
96 if let Claims::IdToken { keys, .. } = &mut self.claims {
97 *keys = Keys::new(KeysAt::Url(url.to_owned()));
98 }
99 self
100 }
101
102 pub fn github(client: (String, Secret<String>)) -> Result<Self> {
104 let (client_id, client_secret) = client;
105 Ok(Self {
106 name: "github".to_owned(),
107 label: "GitHub".to_owned(),
108 authorization_url: "https://github.com/login/oauth/authorize".to_owned(),
109 token_url: "https://github.com/login/oauth/access_token".to_owned(),
110 claims: Claims::UserEndpoints {
111 user: "https://api.github.com/user".to_owned(),
112 emails: "https://api.github.com/user/emails".to_owned(),
113 },
114 client_id,
115 client_secret,
116 scopes: "read:user user:email".to_owned(),
117 })
118 }
119
120 #[must_use]
121 pub fn name(&self) -> &str {
122 &self.name
123 }
124
125 pub(crate) fn requested_urls(&self) -> Vec<&str> {
127 let mut urls = vec![self.token_url.as_str()];
128 match &self.claims {
129 Claims::IdToken { issuer, keys } => urls.push(keys.url().unwrap_or(issuer)),
130 Claims::UserEndpoints { user, emails } => urls.extend([user.as_str(), emails.as_str()]),
131 }
132 urls
133 }
134}