1use super::support::*;
2use super::RegistryAuthStore;
3use crate::auth::{
4 AuthPasskeyChallengeResponse, AuthPasskeyEnrollmentResponse, AuthPasskeyLoginResponse,
5 AuthSessionKind, AuthSshKeyChallengeResponse, AuthSshKeyEnrollmentResponse,
6 AuthSshKeyLoginResponse,
7};
8use crate::{PrayError, PrayResult};
9use rusqlite::OptionalExtension;
10
11impl RegistryAuthStore {
12 pub fn request_passkey_challenge(
13 &self,
14 credential_id: &str,
15 ) -> PrayResult<AuthPasskeyChallengeResponse> {
16 validate_identifier(credential_id, "credential id")?;
17 let connection = self.connection()?;
18 let email: String = connection.query_row(
19 "SELECT email FROM passkeys WHERE credential_id = ?1",
20 rusqlite::params![credential_id],
21 |row| row.get(0),
22 )?;
23 let challenge = generate_auth_challenge("passkey", credential_id)?;
24 let challenge_id = generate_challenge_id(&email, credential_id, "passkey", &challenge)?;
25 store_challenge(&connection, &challenge_id, &email, &challenge, "passkey")?;
26 Ok(AuthPasskeyChallengeResponse {
27 credential_id: credential_id.to_string(),
28 challenge_id,
29 challenge,
30 })
31 }
32 pub fn respond_passkey_challenge(
33 &self,
34 credential_id: &str,
35 challenge_id: &str,
36 signature: &str,
37 ) -> PrayResult<AuthPasskeyLoginResponse> {
38 validate_identifier(credential_id, "credential id")?;
39 validate_identifier(challenge_id, "challenge id")?;
40 validate_signature(signature)?;
41 let connection = self.connection()?;
42 let email: String = connection.query_row(
43 "SELECT email FROM passkeys WHERE credential_id = ?1",
44 rusqlite::params![credential_id],
45 |row| row.get(0),
46 )?;
47 let challenge = load_challenge(&connection, challenge_id, &email, "passkey")?;
48 let public_key = load_passkey_public_key(&connection, credential_id)?;
49 verify_signature(&public_key, challenge.challenge.as_bytes(), signature)?;
50 mark_challenge_used(&connection, challenge_id)?;
51 let session = self.issue_session(&email, AuthSessionKind::Passkey)?;
52 Ok(AuthPasskeyLoginResponse {
53 email,
54 token: session.token,
55 })
56 }
57 pub fn request_ssh_key_challenge(
58 &self,
59 public_key: &str,
60 ) -> PrayResult<AuthSshKeyChallengeResponse> {
61 validate_public_key(public_key)?;
62 let connection = self.connection()?;
63 let (public_key, _) = parse_ssh_ed25519_public_key(public_key)?;
64 let fingerprint = ssh_key_fingerprint(&public_key);
65 let email: String = connection.query_row(
66 "SELECT email FROM ssh_keys WHERE fingerprint = ?1",
67 rusqlite::params![fingerprint],
68 |row| row.get(0),
69 )?;
70 let challenge = generate_auth_challenge("ssh_key", &public_key)?;
71 let challenge_id = generate_challenge_id(&email, &fingerprint, "ssh_key", &challenge)?;
72 store_challenge(&connection, &challenge_id, &email, &challenge, "ssh_key")?;
73 Ok(AuthSshKeyChallengeResponse {
74 fingerprint,
75 challenge_id,
76 challenge,
77 })
78 }
79 pub fn respond_ssh_key_challenge(
80 &self,
81 public_key: &str,
82 challenge_id: &str,
83 signature: &str,
84 ) -> PrayResult<AuthSshKeyLoginResponse> {
85 validate_public_key(public_key)?;
86 validate_identifier(challenge_id, "challenge id")?;
87 validate_signature(signature)?;
88 let connection = self.connection()?;
89 let (public_key, _) = parse_ssh_ed25519_public_key(public_key)?;
90 let fingerprint = ssh_key_fingerprint(&public_key);
91 let email: String = connection.query_row(
92 "SELECT email FROM ssh_keys WHERE fingerprint = ?1",
93 rusqlite::params![fingerprint],
94 |row| row.get(0),
95 )?;
96 let challenge = load_challenge(&connection, challenge_id, &email, "ssh_key")?;
97 verify_signature(&public_key, challenge.challenge.as_bytes(), signature)?;
98 mark_challenge_used(&connection, challenge_id)?;
99 let session = self.issue_session(&email, AuthSessionKind::SshKey)?;
100 Ok(AuthSshKeyLoginResponse {
101 email,
102 token: session.token,
103 })
104 }
105 pub fn enroll_passkey(
106 &self,
107 email: &str,
108 credential_id: &str,
109 public_key: &str,
110 label: Option<&str>,
111 ) -> PrayResult<AuthPasskeyEnrollmentResponse> {
112 validate_email(email)?;
113 validate_identifier(credential_id, "credential id")?;
114 validate_public_key(public_key)?;
115 let connection = self.connection()?;
116 ensure_user_can_authenticate(&connection, email)?;
117 let timestamp = current_unix_timestamp()?;
118 connection.execute(
119 "INSERT INTO passkeys (credential_id, email, public_key, label, created_at, last_used_at)
120 VALUES (?1, ?2, ?3, ?4, ?5, NULL)
121 ON CONFLICT(credential_id) DO UPDATE SET email = excluded.email, public_key = excluded.public_key, label = excluded.label",
122 rusqlite::params![credential_id, email, public_key, label.unwrap_or(""), timestamp],
123 )?;
124 Ok(AuthPasskeyEnrollmentResponse {
125 email: email.to_string(),
126 credential_id: credential_id.to_string(),
127 enrolled: true,
128 })
129 }
130 pub fn login_with_passkey(&self, credential_id: &str) -> PrayResult<AuthPasskeyLoginResponse> {
131 validate_identifier(credential_id, "credential id")?;
132 let connection = self.connection()?;
133 let email: Option<String> = connection
134 .query_row(
135 "SELECT email FROM passkeys WHERE credential_id = ?1",
136 rusqlite::params![credential_id],
137 |row| row.get(0),
138 )
139 .optional()?;
140 let Some(email) = email else {
141 return Err(PrayError::Resolution(format!(
142 "unknown passkey credential: {credential_id}"
143 )));
144 };
145 let session = self.issue_session(&email, AuthSessionKind::Passkey)?;
146 connection.execute(
147 "UPDATE passkeys SET last_used_at = ?2 WHERE credential_id = ?1",
148 rusqlite::params![credential_id, current_unix_timestamp()?],
149 )?;
150 Ok(AuthPasskeyLoginResponse {
151 email,
152 token: session.token,
153 })
154 }
155 pub fn enroll_ssh_key(
156 &self,
157 email: &str,
158 public_key: &str,
159 label: Option<&str>,
160 ) -> PrayResult<AuthSshKeyEnrollmentResponse> {
161 validate_email(email)?;
162 validate_public_key(public_key)?;
163 let connection = self.connection()?;
164 ensure_user_can_authenticate(&connection, email)?;
165 let (public_key, _) = parse_ssh_ed25519_public_key(public_key)?;
166 let fingerprint = ssh_key_fingerprint(&public_key);
167 let timestamp = current_unix_timestamp()?;
168 connection.execute(
169 "INSERT INTO ssh_keys (fingerprint, email, public_key, label, created_at, last_used_at)
170 VALUES (?1, ?2, ?3, ?4, ?5, NULL)
171 ON CONFLICT(fingerprint) DO UPDATE SET email = excluded.email, public_key = excluded.public_key, label = excluded.label",
172 rusqlite::params![fingerprint, email, public_key, label.unwrap_or(""), timestamp],
173 )?;
174 Ok(AuthSshKeyEnrollmentResponse {
175 email: email.to_string(),
176 fingerprint,
177 enrolled: true,
178 })
179 }
180 pub fn login_with_ssh_key(&self, public_key: &str) -> PrayResult<AuthSshKeyLoginResponse> {
181 validate_public_key(public_key)?;
182 let connection = self.connection()?;
183 let (public_key, _) = parse_ssh_ed25519_public_key(public_key)?;
184 let fingerprint = ssh_key_fingerprint(&public_key);
185 let email: Option<String> = connection
186 .query_row(
187 "SELECT email FROM ssh_keys WHERE fingerprint = ?1",
188 rusqlite::params![fingerprint],
189 |row| row.get(0),
190 )
191 .optional()?;
192 let Some(email) = email else {
193 return Err(PrayError::Resolution(format!(
194 "unknown ssh key fingerprint: {fingerprint}"
195 )));
196 };
197 let session = self.issue_session(&email, AuthSessionKind::SshKey)?;
198 connection.execute(
199 "UPDATE ssh_keys SET last_used_at = ?2 WHERE fingerprint = ?1",
200 rusqlite::params![fingerprint, current_unix_timestamp()?],
201 )?;
202 Ok(AuthSshKeyLoginResponse {
203 email,
204 token: session.token,
205 })
206 }
207}