alibi_plugins/sso/
domain_confirmation.rs1use super::{
3 AuthContext, AuthError, AuthResult, AuthSchema, EndpointResponse, JsValue, ServerEndpoint,
4 SessionRequest, SsoPlugin, Value, json,
5};
6use hickory_resolver::{
7 TokioResolver,
8 config::{NameServerConfig, ResolverConfig},
9 net::runtime::TokioRuntimeProvider,
10};
11impl SsoPlugin {
12 pub fn verify_domain_endpoint(provider_id: &str) -> AuthResult<ServerEndpoint<Value>> {
13 ServerEndpoint::new("sso", "verifyDomain").with_body(&json!({"providerId":provider_id}))
14 }
15 pub(super) async fn verify_domain<S: AuthSchema>(
16 &self,
17 input: &JsValue,
18 request: &impl SessionRequest,
19 ctx: &AuthContext<S>,
20 ) -> AuthResult<EndpointResponse> {
21 let provider = self.authorized_provider(input, request, ctx).await?;
22 if provider.domain_verified == Some(true) {
23 return Err(error(
24 409,
25 "DOMAIN_VERIFIED",
26 "Domain has already been verified",
27 ));
28 }
29 let identifier = self.domain_identifier(&provider.provider_id);
30 if identifier.encode_utf16().count() > 63 {
31 return Err(error(
32 400,
33 "IDENTIFIER_TOO_LONG",
34 "Verification identifier exceeds the DNS label limit of 63 characters",
35 ));
36 }
37 let proof = ctx
38 .verifications()
39 .find(&identifier)
40 .await?
41 .filter(|proof| {
42 proof
43 .expires_at()
44 .is_ok_and(|expiry| expiry > chrono::Utc::now())
45 })
46 .ok_or_else(|| {
47 error(
48 404,
49 "NO_PENDING_VERIFICATION",
50 "No pending domain verification exists",
51 )
52 })?;
53 let resolver = if self.config.domain_verification_dns_servers.is_empty() {
54 TokioResolver::builder_tokio().map_err(|_| {
55 error(
56 500,
57 "DOMAIN_VERIFICATION_FAILED",
58 "Unable to verify domain ownership due to server error",
59 )
60 })?
61 } else {
62 let servers = self
63 .config
64 .domain_verification_dns_servers
65 .iter()
66 .map(|address| {
67 let mut server = NameServerConfig::udp(address.ip());
68 for connection in &mut server.connections {
69 connection.port = address.port();
70 }
71 server
72 })
73 .collect();
74 TokioResolver::builder_with_config(
75 ResolverConfig::from_name_servers(servers),
76 TokioRuntimeProvider::default(),
77 )
78 }
79 .build()
80 .map_err(|_| {
81 error(
82 500,
83 "DOMAIN_VERIFICATION_FAILED",
84 "Unable to verify domain ownership due to server error",
85 )
86 })?;
87 let domains = domains(&provider.domain)?;
88 let token = proof.value()?;
89 let record = format!("{}={token}", proof.identifier()?);
90 for domain in domains {
91 let answers = resolver.txt_lookup(format!("{identifier}.{domain}.")).await;
92 let matched = answers.is_ok_and(|answers| {
93 answers.answers().iter().any(|answer| {
94 if let hickory_resolver::proto::rr::RData::TXT(txt) = &answer.data {
95 let joined = txt
96 .txt_data
97 .iter()
98 .flat_map(|chunk| chunk.iter().copied())
99 .collect::<Vec<u8>>();
100 String::from_utf8_lossy(&joined).trim() == record
101 || String::from_utf8_lossy(&joined).trim() == token
102 } else {
103 false
104 }
105 })
106 });
107 if !matched {
108 return Err(error(
109 502,
110 "DOMAIN_VERIFICATION_FAILED",
111 &format!("Unable to verify domain ownership for {domain}. Try again later"),
112 ));
113 }
114 }
115 if !ctx
116 .database
117 .mark_sso_domain_verified(&provider.id, &provider.domain)
118 .await?
119 {
120 return Err(error(
121 409,
122 "SSO_PROVIDER_CHANGED",
123 "SSO provider changed while domain verification was in progress. Reload the provider and try again",
124 ));
125 }
126 let mut response = EndpointResponse::empty()
127 .with_status(204)
128 .with_header("Content-Type", "application/json");
129 if let Some(http) = response.http_response_mut() {
130 http.status = 204;
131 }
132 Ok(response)
133 }
134}
135fn error(status: u16, code: &str, message: &str) -> AuthError {
136 AuthError::Api {
137 status,
138 code: Some(code.into()),
139 message: message.into(),
140 }
141}
142fn domains(raw: &str) -> AuthResult<Vec<String>> {
143 let mut values = Vec::new();
144 for item in raw
145 .split(',')
146 .map(str::trim)
147 .filter(|item| !item.is_empty())
148 {
149 let url = if item.contains("://") {
150 url::Url::parse(item)
151 } else {
152 url::Url::parse(&format!("https://{item}"))
153 }
154 .map_err(|_| error(400, "INVALID_DOMAIN", "Invalid domain"))?;
155 let domain = url
156 .host_str()
157 .filter(|host| !host.is_empty())
158 .ok_or_else(|| error(400, "INVALID_DOMAIN", "Invalid domain"))?
159 .to_ascii_lowercase();
160 if !values.contains(&domain) {
161 values.push(domain);
162 }
163 }
164 if values.is_empty() {
165 return Err(error(400, "INVALID_DOMAIN", "Invalid domain"));
166 }
167 Ok(values)
168}