1- use std:: { future:: Future , pin:: Pin , str:: FromStr , sync:: Arc } ;
1+ use std:: {
2+ future:: Future ,
3+ hash:: { DefaultHasher , Hash , Hasher } ,
4+ pin:: Pin ,
5+ str:: FromStr ,
6+ sync:: Arc ,
7+ } ;
28
39use crate :: { app_config:: AppConfig , AppState } ;
410use actix_web:: {
11+ cookie:: Cookie ,
512 dev:: { forward_ready, Service , ServiceRequest , ServiceResponse , Transform } ,
613 middleware:: Condition ,
714 web:: { self , Query } ,
@@ -10,11 +17,13 @@ use actix_web::{
1017use anyhow:: { anyhow, Context } ;
1118use awc:: Client ;
1219use openidconnect:: {
13- core:: { CoreAuthDisplay , CoreAuthenticationFlow , CoreGenderClaim , CoreIdToken } ,
20+ core:: { CoreAuthenticationFlow , CoreGenderClaim , CoreIdToken } ,
21+ url:: Url ,
1422 AsyncHttpClient , CsrfToken , EmptyAdditionalClaims , EndpointMaybeSet , EndpointNotSet ,
15- EndpointSet , IdToken , IdTokenClaims , IssuerUrl , Nonce , OAuth2TokenResponse , RedirectUrl , Scope ,
23+ EndpointSet , IdTokenClaims , IssuerUrl , Nonce , OAuth2TokenResponse , RedirectUrl , Scope ,
1624 TokenResponse ,
1725} ;
26+ use password_hash:: { rand_core:: OsRng , SaltString } ;
1827use serde:: { Deserialize , Serialize } ;
1928
2029use super :: http_client:: make_http_client;
@@ -208,18 +217,9 @@ impl<S> OidcService<S> {
208217
209218 log:: debug!( "Redirecting to OIDC provider" ) ;
210219
211- let auth_url = build_auth_url (
212- & self . oidc_client ,
213- & self . config . scopes ,
214- request. path ( ) . to_string ( ) ,
215- ) ;
216- Box :: pin ( async move {
217- let state_cookie = create_state_cookie ( & request) ;
218- let mut response = build_redirect_response ( auth_url) ;
219-
220- response. add_cookie ( & state_cookie) ?;
221- Ok ( request. into_response ( response) )
222- } )
220+ let response =
221+ build_auth_provider_redirect_response ( & self . oidc_client , & self . config , & request) ;
222+ Box :: pin ( async move { Ok ( request. into_response ( response) ) } )
223223 }
224224
225225 fn handle_oidc_callback (
@@ -236,12 +236,9 @@ impl<S> OidcService<S> {
236236 Ok ( response) => Ok ( request. into_response ( response) ) ,
237237 Err ( e) => {
238238 log:: error!( "Failed to process OIDC callback with params {query_string}: {e}" ) ;
239- let auth_url = build_auth_url (
240- & oidc_client,
241- & oidc_config. scopes ,
242- request. path ( ) . to_string ( ) ,
243- ) ;
244- Ok ( request. into_response ( build_redirect_response ( auth_url) ) )
239+ let resp =
240+ build_auth_provider_redirect_response ( & oidc_client, & oidc_config, & request) ;
241+ Ok ( request. into_response ( resp) )
245242 }
246243 }
247244 } )
@@ -301,6 +298,12 @@ async fn process_oidc_callback(
301298 )
302299 } ) ?
303300 . into_inner ( ) ;
301+
302+ if state. csrf_token . secret ( ) != params. state . secret ( ) {
303+ log:: debug!( "CSRF token mismatch: expected {state:?}, got {params:?}" ) ;
304+ return Err ( anyhow ! ( "Invalid CSRF token: {}" , params. state. secret( ) ) ) ;
305+ }
306+
304307 log:: debug!( "Processing OIDC callback with params: {params:?}. Requesting token..." ) ;
305308 let token_response = exchange_code_for_token ( oidc_client, http_client, params) . await ?;
306309 log:: debug!( "Received token response: {token_response:?}" ) ;
@@ -337,7 +340,7 @@ fn set_auth_cookie(
337340
338341 let id_token_str = id_token. to_string ( ) ;
339342 log:: trace!( "Setting auth cookie: {SQLPAGE_AUTH_COOKIE_NAME}=\" {id_token_str}\" " ) ;
340- let cookie = actix_web :: cookie :: Cookie :: build ( SQLPAGE_AUTH_COOKIE_NAME , id_token_str)
343+ let cookie = Cookie :: build ( SQLPAGE_AUTH_COOKIE_NAME , id_token_str)
341344 . secure ( true )
342345 . http_only ( true )
343346 . same_site ( actix_web:: cookie:: SameSite :: Lax )
@@ -348,6 +351,19 @@ fn set_auth_cookie(
348351 Ok ( ( ) )
349352}
350353
354+ fn build_auth_provider_redirect_response (
355+ oidc_client : & OidcClient ,
356+ oidc_config : & Arc < OidcConfig > ,
357+ request : & ServiceRequest ,
358+ ) -> HttpResponse {
359+ let AuthUrl { url, params } = build_auth_url ( oidc_client, & oidc_config. scopes ) ;
360+ let state_cookie = create_state_cookie ( request, params) ;
361+ HttpResponse :: TemporaryRedirect ( )
362+ . append_header ( ( "Location" , url. to_string ( ) ) )
363+ . cookie ( state_cookie)
364+ . body ( "Redirecting..." )
365+ }
366+
351367fn build_redirect_response ( target_url : String ) -> HttpResponse {
352368 HttpResponse :: TemporaryRedirect ( )
353369 . append_header ( ( "Location" , target_url) )
@@ -496,33 +512,79 @@ fn make_oidc_client(
496512#[ derive( Debug , Deserialize ) ]
497513struct OidcCallbackParams {
498514 code : String ,
499- state : String ,
515+ state : CsrfToken ,
500516}
501517
502- fn build_auth_url ( oidc_client : & OidcClient , scopes : & [ Scope ] , initial_url : String ) -> String {
503- let ( auth_url, csrf_token, nonce) = oidc_client
518+ struct AuthUrl {
519+ url : Url ,
520+ params : AuthUrlParams ,
521+ }
522+
523+ struct AuthUrlParams {
524+ csrf_token : CsrfToken ,
525+ nonce : Nonce ,
526+ }
527+
528+ fn build_auth_url ( oidc_client : & OidcClient , scopes : & [ Scope ] ) -> AuthUrl {
529+ let nonce_source = Nonce :: new_random ( ) ;
530+ let hashed_nonce = Nonce :: new ( hash_nonce ( & nonce_source) ) ;
531+ let ( url, csrf_token, _nonce) = oidc_client
504532 . authorize_url (
505533 CoreAuthenticationFlow :: AuthorizationCode ,
506534 CsrfToken :: new_random,
507- Nonce :: new_random ,
535+ || hashed_nonce ,
508536 )
509537 . add_scopes ( scopes. iter ( ) . cloned ( ) )
510538 . url ( ) ;
511- auth_url. to_string ( )
539+ AuthUrl {
540+ url,
541+ params : AuthUrlParams {
542+ csrf_token,
543+ nonce : nonce_source,
544+ } ,
545+ }
512546}
513547
514548#[ derive( Debug , Serialize , Deserialize ) ]
515549struct OidcLoginState {
550+ /// The URL to redirect to after the login process is complete.
516551 #[ serde( rename = "u" ) ]
517552 initial_url : String ,
553+ /// The CSRF token to use for the login process.
554+ #[ serde( rename = "c" ) ]
555+ csrf_token : CsrfToken ,
556+ /// The source nonce to use for the login process. It must be checked against the hash
557+ /// stored in the ID token.
558+ #[ serde( rename = "n" ) ]
559+ nonce : Nonce ,
518560}
519561
520- fn create_state_cookie ( request : & ServiceRequest ) -> actix_web:: cookie:: Cookie {
521- let state = OidcLoginState {
522- initial_url : request. path ( ) . to_string ( ) ,
523- } ;
562+ fn hash_nonce ( nonce : & Nonce ) -> String {
563+ use argon2:: password_hash:: { rand_core:: OsRng , PasswordHasher , SaltString } ;
564+ let salt = SaltString :: generate ( & mut OsRng ) ;
565+ // low-cost parameters
566+ let params = argon2:: Params :: new ( 8 , 1 , 1 , None ) . expect ( "bug: invalid Argon2 parameters" ) ;
567+ let argon2 = argon2:: Argon2 :: new ( argon2:: Algorithm :: Argon2id , argon2:: Version :: V0x13 , params) ;
568+ let hash = argon2
569+ . hash_password ( nonce. secret ( ) . as_bytes ( ) , & salt)
570+ . expect ( "bug: failed to hash nonce" ) ;
571+ hash. to_string ( )
572+ }
573+
574+ impl OidcLoginState {
575+ fn new ( request : & ServiceRequest , auth_url : AuthUrlParams ) -> Self {
576+ Self {
577+ initial_url : request. path ( ) . to_string ( ) ,
578+ csrf_token : auth_url. csrf_token ,
579+ nonce : auth_url. nonce ,
580+ }
581+ }
582+ }
583+
584+ fn create_state_cookie ( request : & ServiceRequest , auth_url : AuthUrlParams ) -> Cookie {
585+ let state = OidcLoginState :: new ( request, auth_url) ;
524586 let state_json = serde_json:: to_string ( & state) . unwrap ( ) ;
525- actix_web :: cookie :: Cookie :: build ( SQLPAGE_STATE_COOKIE_NAME , state_json)
587+ Cookie :: build ( SQLPAGE_STATE_COOKIE_NAME , state_json)
526588 . secure ( true )
527589 . http_only ( true )
528590 . same_site ( actix_web:: cookie:: SameSite :: Lax )
0 commit comments