1#[path = "auth_store_keys.rs"]
2mod keys;
3#[path = "auth_store_support.rs"]
4mod support;
5#[path = "auth_store_tokens.rs"]
6mod tokens;
7
8use support::*;
9
10pub use tokens::{bearer_token_from_authorization, PublishTokenRecord, PUBLISH_SCOPE};
11
12use crate::auth::{
13 AuthRegistrationResponse, AuthSessionKind, AuthSessionResponse, AuthVerificationResponse,
14};
15use crate::trust::EmailConfirmationPolicy;
16use crate::{PrayError, PrayResult};
17use rusqlite::{Connection, OptionalExtension};
18use std::fs;
19use std::path::{Path, PathBuf};
20
21#[derive(Debug, Clone)]
22pub struct RegistryAuthStore {
23 database_path: PathBuf,
24}
25
26impl RegistryAuthStore {
27 pub fn open(root: &Path) -> PrayResult<Self> {
28 let database_path = root.join(".pray/auth.db");
29 if let Some(parent) = database_path.parent() {
30 fs::create_dir_all(parent)?;
31 }
32 let store = Self { database_path };
33 store.initialize()?;
34 Ok(store)
35 }
36 pub fn register_email(
37 &self,
38 email: &str,
39 policy: EmailConfirmationPolicy,
40 ) -> PrayResult<AuthRegistrationResponse> {
41 validate_email(email)?;
42 let connection = self.connection()?;
43 let timestamp = current_unix_timestamp()?;
44 let verified = matches!(policy, EmailConfirmationPolicy::Disabled);
45 let verification_code = if verified {
46 None
47 } else {
48 Some(generate_verification_code(email, timestamp))
49 };
50 let policy_text = email_confirmation_policy_text(policy);
51
52 connection.execute(
53 "INSERT INTO users (email, email_verified, email_confirmation_policy, created_at) VALUES (?1, ?2, ?3, ?4)
54 ON CONFLICT(email) DO UPDATE SET email_verified = excluded.email_verified, email_confirmation_policy = excluded.email_confirmation_policy",
55 rusqlite::params![email, verified, policy_text, timestamp],
56 )?;
57 if let Some(code) = verification_code.as_ref() {
58 connection.execute(
59 "INSERT INTO email_verification_codes (email, code, created_at, verified_at)
60 VALUES (?1, ?2, ?3, NULL)
61 ON CONFLICT(email) DO UPDATE SET code = excluded.code, created_at = excluded.created_at, verified_at = NULL",
62 rusqlite::params![email, code, timestamp],
63 )?;
64 }
65
66 Ok(AuthRegistrationResponse {
67 email: email.to_string(),
68 verified,
69 verification_code,
70 })
71 }
72 pub fn verify_email(&self, email: &str, code: &str) -> PrayResult<AuthVerificationResponse> {
73 validate_email(email)?;
74 if code.trim().is_empty() {
75 return Err(PrayError::Unsupported(
76 "verification code cannot be empty".to_string(),
77 ));
78 }
79 let connection = self.connection()?;
80 let stored_code: Option<String> = connection
81 .query_row(
82 "SELECT code FROM email_verification_codes WHERE email = ?1",
83 rusqlite::params![email],
84 |row| row.get(0),
85 )
86 .optional()?;
87 let Some(stored_code) = stored_code else {
88 return Err(PrayError::Resolution(format!(
89 "no verification code found for {email}"
90 )));
91 };
92 if stored_code != code {
93 return Err(PrayError::Resolution(format!(
94 "verification code mismatch for {email}"
95 )));
96 }
97 let timestamp = current_unix_timestamp()?;
98 connection.execute(
99 "UPDATE users SET email_verified = 1 WHERE email = ?1",
100 rusqlite::params![email],
101 )?;
102 connection.execute(
103 "UPDATE email_verification_codes SET verified_at = ?2 WHERE email = ?1",
104 rusqlite::params![email, timestamp],
105 )?;
106 Ok(AuthVerificationResponse {
107 email: email.to_string(),
108 verified: true,
109 })
110 }
111 pub fn user_verified(&self, email: &str) -> PrayResult<bool> {
112 validate_email(email)?;
113 let connection = self.connection()?;
114 let verified: Option<bool> = connection
115 .query_row(
116 "SELECT email_verified FROM users WHERE email = ?1",
117 rusqlite::params![email],
118 |row| row.get(0),
119 )
120 .optional()?;
121 Ok(verified.unwrap_or(false))
122 }
123 pub fn issue_session(
124 &self,
125 email: &str,
126 kind: AuthSessionKind,
127 ) -> PrayResult<AuthSessionResponse> {
128 validate_email(email)?;
129 let connection = self.connection()?;
130 let user: Option<(bool, String)> = connection
131 .query_row(
132 "SELECT email_verified, email_confirmation_policy FROM users WHERE email = ?1",
133 rusqlite::params![email],
134 |row| Ok((row.get(0)?, row.get(1)?)),
135 )
136 .optional()?;
137 let Some((verified, policy)) = user else {
138 return Err(PrayError::Resolution(format!("unknown user: {email}")));
139 };
140 if !verified && policy != email_confirmation_policy_text(EmailConfirmationPolicy::Optional)
141 {
142 return Err(PrayError::Resolution(format!(
143 "email confirmation required for {email}"
144 )));
145 }
146 let timestamp = current_unix_timestamp()?;
147 let token = generate_session_token(email, &kind, timestamp);
148 connection.execute(
149 "INSERT INTO sessions (token, email, kind, created_at, last_used_at)
150 VALUES (?1, ?2, ?3, ?4, ?4)
151 ON CONFLICT(token) DO UPDATE SET last_used_at = excluded.last_used_at",
152 rusqlite::params![token, email, auth_session_kind_text(&kind), timestamp],
153 )?;
154 Ok(AuthSessionResponse {
155 email: email.to_string(),
156 token,
157 kind,
158 })
159 }
160 pub fn resolve_session(&self, token: &str) -> PrayResult<Option<AuthSessionResponse>> {
161 if token.trim().is_empty() {
162 return Ok(None);
163 }
164 let connection = self.connection()?;
165 let session: Option<(String, String)> = connection
166 .query_row(
167 "SELECT email, kind FROM sessions WHERE token = ?1",
168 rusqlite::params![token],
169 |row| Ok((row.get(0)?, row.get(1)?)),
170 )
171 .optional()?;
172 let Some((email, kind_text)) = session else {
173 return Ok(None);
174 };
175 let kind = parse_auth_session_kind(&kind_text)?;
176 let timestamp = current_unix_timestamp()?;
177 connection.execute(
178 "UPDATE sessions SET last_used_at = ?2 WHERE token = ?1",
179 rusqlite::params![token, timestamp],
180 )?;
181 Ok(Some(AuthSessionResponse {
182 email,
183 token: token.to_string(),
184 kind,
185 }))
186 }
187 fn initialize(&self) -> PrayResult<()> {
188 let connection = self.connection()?;
189 connection.execute_batch(
190 "CREATE TABLE IF NOT EXISTS users (
191 email TEXT PRIMARY KEY,
192 email_verified INTEGER NOT NULL,
193 email_confirmation_policy TEXT NOT NULL,
194 created_at INTEGER NOT NULL
195 );
196 CREATE TABLE IF NOT EXISTS email_verification_codes (
197 email TEXT PRIMARY KEY,
198 code TEXT NOT NULL,
199 created_at INTEGER NOT NULL,
200 verified_at INTEGER
201 );
202 CREATE TABLE IF NOT EXISTS passkeys (
203 credential_id TEXT PRIMARY KEY,
204 email TEXT NOT NULL,
205 public_key TEXT NOT NULL,
206 label TEXT,
207 created_at INTEGER NOT NULL,
208 last_used_at INTEGER,
209 FOREIGN KEY(email) REFERENCES users(email) ON DELETE CASCADE
210 );
211 CREATE TABLE IF NOT EXISTS ssh_keys (
212 fingerprint TEXT PRIMARY KEY,
213 email TEXT NOT NULL,
214 public_key TEXT NOT NULL,
215 label TEXT,
216 created_at INTEGER NOT NULL,
217 last_used_at INTEGER,
218 FOREIGN KEY(email) REFERENCES users(email) ON DELETE CASCADE
219 );
220 CREATE TABLE IF NOT EXISTS sessions (
221 token TEXT PRIMARY KEY,
222 email TEXT NOT NULL,
223 kind TEXT NOT NULL,
224 created_at INTEGER NOT NULL,
225 last_used_at INTEGER,
226 FOREIGN KEY(email) REFERENCES users(email) ON DELETE CASCADE
227 );
228 CREATE TABLE IF NOT EXISTS auth_challenges (
229 challenge_id TEXT PRIMARY KEY,
230 email TEXT NOT NULL,
231 kind TEXT NOT NULL,
232 challenge TEXT NOT NULL,
233 created_at INTEGER NOT NULL,
234 used_at INTEGER,
235 FOREIGN KEY(email) REFERENCES users(email) ON DELETE CASCADE
236 );
237 CREATE TABLE IF NOT EXISTS publish_tokens (
238 token TEXT PRIMARY KEY,
239 email TEXT NOT NULL,
240 scopes TEXT NOT NULL,
241 created_at INTEGER NOT NULL,
242 last_used_at INTEGER,
243 FOREIGN KEY(email) REFERENCES users(email) ON DELETE CASCADE
244 );",
245 )?;
246 Ok(())
247 }
248 fn connection(&self) -> PrayResult<Connection> {
249 let connection = Connection::open(&self.database_path)?;
250 connection.pragma_update(None, "foreign_keys", "ON")?;
251 Ok(connection)
252 }
253}
254
255pub use support::ssh_public_key_fingerprint_text;