@@ -22,14 +22,19 @@ pub enum AuthError {
2222}
2323
2424pub struct AuthService {
25- admin_user : String ,
26- admin_password_hash : String ,
25+ fallback_admin : Option < FallbackAdmin > ,
2726 sessions : Mutex < HashSet < SessionRecord > > ,
2827 session_seq : AtomicU64 ,
2928 session_ttl_secs : u64 ,
3029 secure_cookie : bool ,
3130}
3231
32+ #[ derive( Debug , Clone ) ]
33+ struct FallbackAdmin {
34+ username : String ,
35+ password_hash : String ,
36+ }
37+
3338#[ derive( Debug , Clone , PartialEq , Eq , Hash ) ]
3439struct SessionRecord {
3540 token : String ,
@@ -43,18 +48,42 @@ impl AuthService {
4348 session_ttl_secs : u64 ,
4449 secure_cookie : bool ,
4550 ) -> Result < Self , String > {
46- if admin_user. trim ( ) . is_empty ( ) {
47- return Err ( "admin username must not be empty" . to_string ( ) ) ;
48- }
49- PasswordHash :: new ( & admin_password_hash)
50- . map_err ( |_| "admin password hash is invalid" . to_string ( ) ) ?;
51+ Self :: new_with_fallback (
52+ Some ( admin_user) ,
53+ Some ( admin_password_hash) ,
54+ session_ttl_secs,
55+ secure_cookie,
56+ )
57+ }
58+
59+ pub fn new_with_fallback (
60+ admin_user : Option < String > ,
61+ admin_password_hash : Option < String > ,
62+ session_ttl_secs : u64 ,
63+ secure_cookie : bool ,
64+ ) -> Result < Self , String > {
5165 if session_ttl_secs == 0 {
5266 return Err ( "session TTL must be >= 1 second" . to_string ( ) ) ;
5367 }
5468
69+ let fallback_admin = match ( admin_user, admin_password_hash) {
70+ ( Some ( username) , Some ( password_hash) ) => {
71+ if username. trim ( ) . is_empty ( ) {
72+ return Err ( "admin username must not be empty" . to_string ( ) ) ;
73+ }
74+ PasswordHash :: new ( & password_hash)
75+ . map_err ( |_| "admin password hash is invalid" . to_string ( ) ) ?;
76+ Some ( FallbackAdmin {
77+ username,
78+ password_hash,
79+ } )
80+ }
81+ ( None , None ) => None ,
82+ _ => return Err ( "ADMIN_USER and ADMIN_PASSWORD_HASH must both be set" . to_string ( ) ) ,
83+ } ;
84+
5585 Ok ( Self {
56- admin_user,
57- admin_password_hash,
86+ fallback_admin,
5887 sessions : Mutex :: new ( HashSet :: new ( ) ) ,
5988 session_seq : AtomicU64 :: new ( 1 ) ,
6089 session_ttl_secs,
@@ -63,30 +92,18 @@ impl AuthService {
6392 }
6493
6594 pub fn dev_default ( ) -> Self {
66- let salt = SaltString :: generate ( & mut OsRng ) ;
67- let password_hash = Argon2 :: default ( )
68- . hash_password ( b"admin" , & salt)
69- . expect ( "default dev password hash should build" )
70- . to_string ( ) ;
95+ let password_hash = hash_password ( "admin" ) . expect ( "default dev password hash should build" ) ;
7196
72- Self :: new ( "admin" . to_string ( ) , password_hash, 3600 , false )
97+ Self :: new_with_fallback ( Some ( "admin" . to_string ( ) ) , Some ( password_hash) , 3600 , false )
7398 . expect ( "dev default auth should be valid" )
7499 }
75100
76101 pub fn login ( & self , username : & str , password : & str ) -> Result < String , AuthError > {
77- if !self . verify_credentials ( username, password) {
102+ if !self . verify_fallback_credentials ( username, password) {
78103 return Err ( AuthError :: InvalidCredentials ) ;
79104 }
80105
81- let token = format ! ( "s-{}" , self . session_seq. fetch_add( 1 , Ordering :: Relaxed ) ) ;
82- let issued_at_utc = now_epoch_seconds ( ) ;
83- if let Ok ( mut sessions) = self . sessions . lock ( ) {
84- sessions. insert ( SessionRecord {
85- token : token. clone ( ) ,
86- issued_at_utc,
87- } ) ;
88- }
89- Ok ( token)
106+ Ok ( self . issue_session_token ( ) )
90107 }
91108
92109 pub fn logout_token ( & self , token : & str ) {
@@ -118,18 +135,30 @@ impl AuthService {
118135 self . secure_cookie
119136 }
120137
121- fn verify_credentials ( & self , username : & str , password : & str ) -> bool {
122- if username != self . admin_user {
123- return false ;
138+ pub fn has_fallback_credentials ( & self ) -> bool {
139+ self . fallback_admin . is_some ( )
140+ }
141+
142+ pub fn issue_session_token ( & self ) -> String {
143+ let token = format ! ( "s-{}" , self . session_seq. fetch_add( 1 , Ordering :: Relaxed ) ) ;
144+ let issued_at_utc = now_epoch_seconds ( ) ;
145+ if let Ok ( mut sessions) = self . sessions . lock ( ) {
146+ sessions. insert ( SessionRecord {
147+ token : token. clone ( ) ,
148+ issued_at_utc,
149+ } ) ;
124150 }
151+ token
152+ }
125153
126- let Ok ( parsed_hash) = PasswordHash :: new ( & self . admin_password_hash ) else {
154+ pub fn verify_fallback_credentials ( & self , username : & str , password : & str ) -> bool {
155+ let Some ( fallback) = self . fallback_admin . as_ref ( ) else {
127156 return false ;
128157 } ;
129-
130- Argon2 :: default ( )
131- . verify_password ( password . as_bytes ( ) , & parsed_hash )
132- . is_ok ( )
158+ if username != fallback . username {
159+ return false ;
160+ }
161+ verify_password ( & fallback . password_hash , password )
133162 }
134163
135164 fn is_expired ( & self , now_utc : i64 , issued_at_utc : i64 ) -> bool {
@@ -143,3 +172,23 @@ fn now_epoch_seconds() -> i64 {
143172 . map ( |duration| duration. as_secs ( ) as i64 )
144173 . unwrap_or_default ( )
145174}
175+
176+ pub fn hash_password ( password : & str ) -> Result < String , String > {
177+ if password. is_empty ( ) {
178+ return Err ( "password must not be empty" . to_string ( ) ) ;
179+ }
180+ let salt = SaltString :: generate ( & mut OsRng ) ;
181+ Argon2 :: default ( )
182+ . hash_password ( password. as_bytes ( ) , & salt)
183+ . map ( |hash| hash. to_string ( ) )
184+ . map_err ( |err| err. to_string ( ) )
185+ }
186+
187+ pub fn verify_password ( password_hash : & str , password : & str ) -> bool {
188+ let Ok ( parsed_hash) = PasswordHash :: new ( password_hash) else {
189+ return false ;
190+ } ;
191+ Argon2 :: default ( )
192+ . verify_password ( password. as_bytes ( ) , & parsed_hash)
193+ . is_ok ( )
194+ }
0 commit comments