rws_server/sso/
discovery.rs1use crate::http_client::Client;
9use super::SsoError;
10
11#[derive(Debug, Clone)]
13pub struct OidcProvider {
14 pub issuer: String,
15 pub authorization_endpoint: String,
16 pub token_endpoint: String,
17 pub jwks_uri: String,
18 pub userinfo_endpoint: Option<String>,
19 pub end_session_endpoint: Option<String>,
20}
21
22impl OidcProvider {
23 pub fn discover(issuer: &str) -> Result<Self, SsoError> {
25 let url = format!(
26 "{}/.well-known/openid-configuration",
27 issuer.trim_end_matches('/')
28 );
29 let resp = Client::new()
30 .get(&url)
31 .timeout_ms(10_000)
32 .send()
33 .map_err(|e| SsoError(format!("discovery fetch failed: {e}")))?;
34 if !resp.is_success() {
35 return Err(SsoError(format!("discovery returned {}", resp.status())));
36 }
37 let body = resp.text().map_err(|e| SsoError(e.to_string()))?;
38 parse_discovery_json(&body)
39 }
40
41 pub fn google() -> Self {
43 OidcProvider {
44 issuer: "https://accounts.google.com".into(),
45 authorization_endpoint: "https://accounts.google.com/o/oauth2/v2/auth".into(),
46 token_endpoint: "https://oauth2.googleapis.com/token".into(),
47 jwks_uri: "https://www.googleapis.com/oauth2/v3/certs".into(),
48 userinfo_endpoint: Some("https://openidconnect.googleapis.com/v1/userinfo".into()),
49 end_session_endpoint: None,
50 }
51 }
52
53 pub fn microsoft(tenant_id: &str) -> Self {
57 let base = format!("https://login.microsoftonline.com/{tenant_id}");
58 OidcProvider {
59 issuer: format!("{base}/v2.0"),
60 authorization_endpoint: format!("{base}/oauth2/v2.0/authorize"),
61 token_endpoint: format!("{base}/oauth2/v2.0/token"),
62 jwks_uri: format!("{base}/discovery/v2.0/keys"),
63 userinfo_endpoint: Some("https://graph.microsoft.com/oidc/userinfo".into()),
64 end_session_endpoint: Some(format!("{base}/oauth2/v2.0/logout")),
65 }
66 }
67
68 pub fn github() -> Self {
73 OidcProvider {
74 issuer: "https://github.com".into(),
75 authorization_endpoint: "https://github.com/login/oauth/authorize".into(),
76 token_endpoint: "https://github.com/login/oauth/access_token".into(),
77 jwks_uri: String::new(),
78 userinfo_endpoint: Some("https://api.github.com/user".into()),
79 end_session_endpoint: None,
80 }
81 }
82
83 pub fn okta(domain: &str) -> Self {
87 let base = format!("https://{}", domain.trim_start_matches("https://"));
88 OidcProvider {
89 issuer: format!("{base}/oauth2/default"),
90 authorization_endpoint: format!("{base}/oauth2/default/v1/authorize"),
91 token_endpoint: format!("{base}/oauth2/default/v1/token"),
92 jwks_uri: format!("{base}/oauth2/default/v1/keys"),
93 userinfo_endpoint: Some(format!("{base}/oauth2/default/v1/userinfo")),
94 end_session_endpoint: Some(format!("{base}/oauth2/default/v1/logout")),
95 }
96 }
97
98 pub fn auth0(domain: &str) -> Self {
102 let base = format!("https://{}", domain.trim_start_matches("https://"));
103 OidcProvider {
104 issuer: format!("{base}/"),
105 authorization_endpoint: format!("{base}/authorize"),
106 token_endpoint: format!("{base}/oauth/token"),
107 jwks_uri: format!("{base}/.well-known/jwks.json"),
108 userinfo_endpoint: Some(format!("{base}/userinfo")),
109 end_session_endpoint: Some(format!("{base}/v2/logout")),
110 }
111 }
112
113 pub fn keycloak(base_url: &str, realm: &str) -> Self {
118 let base = format!("{}/realms/{}", base_url.trim_end_matches('/'), realm);
119 OidcProvider {
120 issuer: base.clone(),
121 authorization_endpoint: format!("{base}/protocol/openid-connect/auth"),
122 token_endpoint: format!("{base}/protocol/openid-connect/token"),
123 jwks_uri: format!("{base}/protocol/openid-connect/certs"),
124 userinfo_endpoint: Some(format!("{base}/protocol/openid-connect/userinfo")),
125 end_session_endpoint: Some(format!("{base}/protocol/openid-connect/logout")),
126 }
127 }
128}
129
130fn parse_discovery_json(json: &str) -> Result<OidcProvider, SsoError> {
133 fn extract(json: &str, key: &str) -> Option<String> {
134 let needle = format!("\"{key}\"");
135 let start = json.find(&needle)? + needle.len();
136 let rest = json[start..].trim_start_matches(|c: char| c.is_whitespace() || c == ':');
137 if rest.starts_with('"') {
138 let inner = &rest[1..];
139 let end = inner.find('"')?;
140 Some(inner[..end].to_string())
141 } else {
142 None
143 }
144 }
145
146 Ok(OidcProvider {
147 issuer: extract(json, "issuer").unwrap_or_default(),
148 authorization_endpoint: extract(json, "authorization_endpoint")
149 .ok_or_else(|| SsoError("missing authorization_endpoint".into()))?,
150 token_endpoint: extract(json, "token_endpoint")
151 .ok_or_else(|| SsoError("missing token_endpoint".into()))?,
152 jwks_uri: extract(json, "jwks_uri")
153 .ok_or_else(|| SsoError("missing jwks_uri".into()))?,
154 userinfo_endpoint: extract(json, "userinfo_endpoint"),
155 end_session_endpoint: extract(json, "end_session_endpoint"),
156 })
157}