1use edda_auth::{
4 DeviceCert, DeviceKey, PublicKey, Purpose, Role, SignedCert,
5 protocol::{
6 Campaign, Challenge, ChallengeRequest, Device, LoginProof, LoginRequest, ServerKey,
7 Session, SetupCode, SetupRequest,
8 },
9};
10use uuid::Uuid;
11
12use crate::{ClientError, check, send};
13
14#[derive(Debug, Clone)]
16pub struct AuthClient {
17 base_url: String,
18 pub(crate) http: reqwest::Client,
19}
20
21impl AuthClient {
22 pub fn new(base_url: impl Into<String>) -> Self {
24 Self {
25 base_url: base_url.into().trim_end_matches('/').to_string(),
26 http: crate::http_client(None),
27 }
28 }
29
30 #[must_use]
32 pub fn with_token(mut self, token: &str) -> Self {
33 self.http = crate::http_client(Some(token));
34 self
35 }
36
37 pub async fn server_key(&self) -> Result<PublicKey, ClientError> {
40 let key: ServerKey = send(self.http.get(self.url("/public-key"))).await?;
41 Ok(key.key)
42 }
43
44 pub async fn wait_for_server_key(
47 &self,
48 patience: std::time::Duration,
49 ) -> Result<PublicKey, ClientError> {
50 let give_up = tokio::time::Instant::now() + patience;
51 loop {
52 match self.server_key().await {
53 Err(ClientError::Http(_)) if tokio::time::Instant::now() < give_up => {
54 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
55 }
56 result => return result,
57 }
58 }
59 }
60
61 pub async fn setup(
63 &self,
64 code: &str,
65 key: &DeviceKey,
66 name: &str,
67 ) -> Result<Campaign, ClientError> {
68 let code = SetupCode {
69 code: code.trim().to_string(),
70 };
71 let campaign: Campaign =
72 send(self.http.post(self.url("/setup/lookup")).json(&code)).await?;
73 let cert = DeviceCert::self_signed(key, campaign.id, Role::Dm, name)
74 .map_err(|e| ClientError::Rejected(e.to_string()))?;
75 let request = SetupRequest {
76 code: code.code,
77 cert,
78 };
79 send(self.http.post(self.url("/setup")).json(&request)).await
80 }
81
82 pub async fn login(
85 &self,
86 campaign: Uuid,
87 key: &DeviceKey,
88 expected_server: Option<PublicKey>,
89 ) -> Result<Session, ClientError> {
90 let request = ChallengeRequest {
91 campaign,
92 key: key.public(),
93 };
94 let challenge: Challenge =
95 send(self.http.post(self.url("/login/challenge")).json(&request)).await?;
96 if challenge.campaign != campaign
97 || expected_server.is_some_and(|expected| expected != challenge.server)
98 {
99 return Err(ClientError::Unauthorized(
100 "the server's key is not the one this device knows; refusing to log in".into(),
101 ));
102 }
103 let signature = key
104 .sign(Purpose::Login, &LoginProof::from(&challenge))
105 .map_err(|e| ClientError::Rejected(e.to_string()))?;
106 let answer = LoginRequest {
107 challenge: challenge.id,
108 signature,
109 };
110 send(self.http.post(self.url("/login")).json(&answer)).await
111 }
112
113 pub async fn campaign(&self) -> Result<Campaign, ClientError> {
115 send(self.http.get(self.url("/campaign"))).await
116 }
117
118 pub async fn devices(&self) -> Result<Vec<Device>, ClientError> {
120 send(self.http.get(self.url("/devices"))).await
121 }
122
123 pub async fn add_device(&self, cert: &SignedCert) -> Result<Device, ClientError> {
126 send(self.http.post(self.url("/devices")).json(cert)).await
127 }
128
129 pub async fn revoke(&self, key: PublicKey) -> Result<Device, ClientError> {
131 let url = self.url(&format!("/devices/{key}"));
132 let response = check(self.http.delete(url).send().await?).await?;
133 response.json().await.map_err(ClientError::InvalidResponse)
134 }
135
136 pub(crate) fn url(&self, path: &str) -> String {
137 format!("{}{path}", self.base_url)
138 }
139}