OAuth 2.0 authorization and token exchange
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Track email_verified in userinfo; require verified email for sign-in

Add email_verified:bool to Oauth.userinfo. Google OIDC sets it from
the email_verified field; GitHub sets it when /user/emails returns a
verified primary; GitLab and custom default to false. The encoder now
faithfully round-trips the verification status.

ocaml-auth now requires email_verified=true to create a user account.
Sign-in is rejected with a clear error if no verified email is
available, instead of fabricating a fake login@provider address.

+43 -19
+31 -10
lib/oauth.ml
··· 347 347 uid : string; 348 348 login : string; 349 349 email : string option; 350 + email_verified : bool; 350 351 name : string; 351 352 avatar_url : string; 352 353 } ··· 361 362 let github_userinfo_jsont = 362 363 Jsont.Object.map ~kind:"github_userinfo" 363 364 (fun id login _email name avatar_url -> 364 - { uid = string_of_int id; login; email = None; name; avatar_url }) 365 + { 366 + uid = string_of_int id; 367 + login; 368 + email = None; 369 + email_verified = false; 370 + name; 371 + avatar_url; 372 + }) 365 373 |> Jsont.Object.mem "id" Jsont.int ~enc:(fun _ -> 0) 366 374 |> Jsont.Object.mem "login" Jsont.string ~dec_absent:"" ~enc:(fun u -> 367 375 u.login) ··· 373 381 |> Jsont.Object.skip_unknown |> Jsont.Object.finish 374 382 375 383 (* Google OIDC: {"sub":"118...","email":"...","email_verified":true,"name":"...","picture":"..."} 376 - Only include the email if email_verified is true. *) 384 + Only populate email when email_verified is true. Track the verified flag. *) 377 385 let google_userinfo_jsont = 378 386 Jsont.Object.map ~kind:"google_userinfo" 379 387 (fun sub email email_verified name picture -> 380 - let email = 381 - match (non_empty email, email_verified) with 382 - | Some e, Some true -> Some e 383 - | _ -> None 384 - in 385 - { uid = sub; login = ""; email; name; avatar_url = picture }) 388 + let verified = email_verified = Some true in 389 + let email = if verified then non_empty email else None in 390 + { 391 + uid = sub; 392 + login = ""; 393 + email; 394 + email_verified = verified; 395 + name; 396 + avatar_url = picture; 397 + }) 386 398 |> Jsont.Object.mem "sub" Jsont.string ~enc:(fun u -> u.uid) 387 399 |> Jsont.Object.mem "email" Jsont.string ~dec_absent:"" ~enc:(fun u -> 388 400 opt_to_string u.email) 389 - |> Jsont.Object.opt_mem "email_verified" Jsont.bool ~enc:(fun _ -> None) 401 + |> Jsont.Object.opt_mem "email_verified" Jsont.bool ~enc:(fun u -> 402 + Some u.email_verified) 390 403 |> Jsont.Object.mem "name" Jsont.string ~dec_absent:"" ~enc:(fun u -> u.name) 391 404 |> Jsont.Object.mem "picture" Jsont.string ~dec_absent:"" ~enc:(fun u -> 392 405 u.avatar_url) ··· 400 413 uid = string_of_int id; 401 414 login = username; 402 415 email = non_empty email; 416 + email_verified = false; 403 417 name; 404 418 avatar_url; 405 419 }) ··· 416 430 (* Custom: uid extracted from the configured uid_field *) 417 431 let custom_userinfo_jsont ~uid_field = 418 432 Jsont.Object.map ~kind:"custom_userinfo" (fun uid email name -> 419 - { uid; login = ""; email = non_empty email; name; avatar_url = "" }) 433 + { 434 + uid; 435 + login = ""; 436 + email = non_empty email; 437 + email_verified = false; 438 + name; 439 + avatar_url = ""; 440 + }) 420 441 |> Jsont.Object.mem uid_field Jsont.string ~enc:(fun u -> u.uid) 421 442 |> Jsont.Object.mem "email" Jsont.string ~dec_absent:"" ~enc:(fun u -> 422 443 opt_to_string u.email)
+12 -9
lib/oauth.mli
··· 278 278 email : string option; 279 279 (** Email address, or [None] if the provider did not return one. 280 280 281 - {b Not verified.} This value comes directly from the provider's 282 - userinfo endpoint and has not been independently verified. For GitHub 283 - in particular, [/user] returns the user's {i public} email which may 284 - be [None]; the verified primary email requires a separate 285 - [GET /user/emails] call with the [user:email] scope. Do not use this 286 - field for authentication decisions without independent verification. 287 - *) 281 + For GitHub, [/user] returns the user's {i public} email which may be 282 + [None]; the verified primary email requires {!parse_github_emails} 283 + with [GET /user/emails]. *) 284 + email_verified : bool; 285 + (** Whether the provider asserts this email is verified. [true] for Google 286 + when [email_verified = true] in the OIDC response, and for emails 287 + obtained via {!parse_github_emails}. [false] for all other cases 288 + including GitLab and custom providers (where verification status is 289 + unknown). Do not use [email] for authentication decisions unless 290 + [email_verified] is [true]. *) 288 291 name : string; (** Display name (may be empty). *) 289 292 avatar_url : string; (** Avatar URL (may be empty). *) 290 293 } 291 294 (** Parsed userinfo from a provider's user profile endpoint. 292 295 293 296 The [uid] is guaranteed non-empty when parsing succeeds. All other fields 294 - are best-effort: they reflect whatever the provider returned and are not 295 - independently verified. *) 297 + are best-effort: they reflect whatever the provider returned. Check 298 + [email_verified] before trusting [email] for authentication. *) 296 299 297 300 val parse_userinfo : provider -> string -> (userinfo, string) result 298 301 (** [parse_userinfo provider body] parses a JSON userinfo response using the