Skip to content

Commit

Permalink
Merge pull request #15 from robur-coop/store-email-verification
Browse files Browse the repository at this point in the history
store email verification uuid in user model, use a random uuid
  • Loading branch information
hannesm authored Aug 12, 2024
2 parents eb9aeaf + a9fc0e7 commit c74ce7a
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 96 deletions.
13 changes: 9 additions & 4 deletions unikernel.ml
Original file line number Diff line number Diff line change
Expand Up @@ -693,15 +693,18 @@ struct
| Some cookie -> (
match Middleware.user_from_auth_cookie cookie users with
| Ok user -> (
let email_verification_uuid = User_model.generate_uuid () in
let updated_user =
User_model.update_user user ~updated_at:now ()
User_model.update_user user ~updated_at:now
~email_verification_uuid:(Some email_verification_uuid)
()
in
Store.update_user !store updated_user >>= function
| Ok store' ->
store := store';
let verification_link =
Utils.Email.generate_verification_link
updated_user.uuid now
email_verification_uuid
in
Logs.info (fun m ->
m "Verification link is: %s" verification_link);
Expand Down Expand Up @@ -733,10 +736,12 @@ struct
let _, (t : Storage.t) = !store in
let users = User_model.create_user_uuid_map t.users in
let now = Ptime.v (P.now_d_ps ()) in
let veification_token =
let verification_token =
String.sub path 19 (String.length path - 19)
in
match User_model.verify_email users veification_token now with
match
User_model.verify_email_token users verification_token now
with
| Ok user -> (
Store.update_user !store user >>= function
| Ok store' ->
Expand Down
108 changes: 45 additions & 63 deletions user_model.ml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type user = {
cookies : cookie list;
created_at : Ptime.t;
updated_at : Ptime.t;
email_verification_uuid : Uuidm.t option;
}

let week = 604800 (* a week = 7 days * 24 hours * 60 minutes * 60 seconds *)
Expand Down Expand Up @@ -184,7 +185,8 @@ let user_of_json = function
get "tokens" xs,
get "cookies" xs,
get "created_at" xs,
get "updated_at" xs )
get "updated_at" xs,
get "email_verification_uuid" xs )
with
| ( Some (`String name),
Some (`String email),
Expand All @@ -194,7 +196,8 @@ let user_of_json = function
Some (`List tokens),
Some (`List cookies),
Some (`String updated_at_str),
Some (`String created_at_str) ) ->
Some (`String created_at_str),
Some email_verification_uuid ) ->
let created_at =
match Utils.TimeHelper.ptime_of_string created_at_str with
| Ok ptime -> Some ptime
Expand Down Expand Up @@ -222,6 +225,18 @@ let user_of_json = function
Ok (cookie :: acc))
(Ok []) cookies
in
let* email_verification_uuid =
match email_verification_uuid with
| `Null -> Ok None
| `String s ->
let* uuid =
Option.to_result
~none:(`Msg "invalid UUID for email verification UUID")
(Uuidm.of_string s)
in
Ok (Some uuid)
| _ -> Error (`Msg "invalid json data for email verification UUID")
in
Ok
{
name;
Expand All @@ -233,6 +248,7 @@ let user_of_json = function
cookies;
created_at = Option.get created_at;
updated_at = Option.get updated_at;
email_verification_uuid;
}
| _ -> Error (`Msg "invalid json for user"))
| _ -> Error (`Msg "invalid json for user")
Expand Down Expand Up @@ -302,13 +318,14 @@ let create_user ~name ~email ~password ~created_at =
cookies = [ session ];
created_at;
updated_at = created_at;
email_verification_uuid = None;
}

let check_if_user_exists email users =
List.find_opt (fun user -> user.email = Utils.Json.clean_string email) users

let update_user user ?name ?email ?email_verified ?password ?tokens ?cookies
?updated_at () =
?updated_at ?email_verification_uuid () =
{
user with
name = (match name with Some name -> name | _ -> user.name);
Expand All @@ -322,6 +339,8 @@ let update_user user ?name ?email ?email_verified ?password ?tokens ?cookies
(match updated_at with
| Some updated_at -> updated_at
| _ -> user.updated_at);
email_verification_uuid =
Option.value ~default:user.email_verification_uuid email_verification_uuid;
}

let update_cookies (cookies : cookie list) (cookie : cookie) : cookie list =
Expand All @@ -335,73 +354,36 @@ let is_valid_cookie (cookie : cookie) now =

let is_email_verified user = Option.is_some user.email_verified

let verify_email users token timestamp =
let token_data = String.split_on_char '/' token in
let uuid =
match List.nth_opt token_data 0 with
| Some user_uuid ->
Logs.err (fun m -> m "email verification: user uuid is %s" user_uuid);
user_uuid
| None ->
Logs.err (fun m -> m "email verification: empty uuid in signature");
""
in

let token_signature =
match List.nth_opt token_data 1 with
| Some token ->
Logs.err (fun m -> m "email verification: token sig is %s" token);
token
| None ->
Logs.err (fun m -> m "email verification: empty signature");
""
in
let decoded_uuid =
match Base64.decode uuid with
| Ok d_uuid ->
Logs.err (fun m ->
m "email verification: decoded user uuid is %s" d_uuid);
d_uuid
| Error (`Msg s) ->
Logs.err (fun m ->
m "email verification: can't decode uuid with error %s" s);
""
let verify_email_token users token timestamp =
let ( let* ) = Result.bind in
let* uuid =
Option.to_result ~none:(`Msg "invalid UUID") (Uuidm.of_string token)
in
let user = find_user_by_key decoded_uuid users in
match user with
| Some u -> (
let is_valid_link =
3600 > Utils.TimeHelper.diff_in_seconds timestamp u.updated_at
in
match is_valid_link with
| true -> (
let computed_signature =
Utils.Email.generate_signature u.uuid u.updated_at
match
List.find_opt
(fun (_, user) ->
match user.email_verification_uuid with
| Some uu -> Uuidm.equal uu uuid
| None -> false)
users
with
| None ->
Logs.err (fun m -> m "email verification: Token couldn't be found.");
Error (`Msg "No token was found.")
| Some (_, u) -> (
match Utils.TimeHelper.diff_in_seconds timestamp u.updated_at < 3600 with
| true ->
let updated_user =
update_user u ~email_verified:(Some timestamp) ~updated_at:timestamp
~email_verification_uuid:None ()
in
Logs.err (fun m ->
m "email verification: token signature is %s" token_signature);
Logs.err (fun m ->
m "email verification: computed signature is %s"
computed_signature);
match String.equal token_signature computed_signature with
| true ->
let updated_user =
update_user u ~email_verified:(Some timestamp)
~updated_at:timestamp ()
in
Ok updated_user
| false ->
Logs.err (fun m -> m "email verification: This link is invalid");
Error (`Msg "This link is invalid."))
Ok updated_user
| false ->
Logs.err (fun m -> m "email verification: This link has expired");
Logs.err (fun m -> m "email verification: This link is expired.");
Error
(`Msg
"This link has expired. Please sign in to get a new verification \
link."))
| None ->
Logs.err (fun m -> m "email verification: This account does not exist.");
Error (`Msg "This account does not exist.")

let user_auth_cookie_from_user (user : user) =
List.find_opt
Expand Down
31 changes: 2 additions & 29 deletions utils.ml
Original file line number Diff line number Diff line change
Expand Up @@ -62,33 +62,6 @@ module Email = struct
Logs.err (fun m -> m "Emile-Email-Validation: %a" Emile.pp_error s);
false

let generate_signature uuid timestamp =
let timestamp_str = TimeHelper.string_of_ptime timestamp in
Base64.encode_string
(Cstruct.to_string
(Mirage_crypto.Hash.SHA256.digest
(Cstruct.of_string (uuid ^ "-" ^ timestamp_str))))

let generate_verification_link uuid timestamp =
let signature = generate_signature uuid timestamp in
let encoded_uuid = Base64.encode_string uuid in
"/auth/verify/token=" ^ encoded_uuid ^ "/" ^ signature

let send_verification_email _email _now =
let config_data =
{
smtp_host = "sandbox.smtp.mailtrap.io";
smtp_port = 25;
username = "2b08693767f1d9";
password = "55168d154ddc56";
}
in
let authentication =
{
Sendmail.mechanism = PLAIN;
username = config_data.username;
password = config_data.password;
}
in
authentication
let generate_verification_link uuid =
"/auth/verify/token=" ^ Uuidm.to_string uuid
end

0 comments on commit c74ce7a

Please sign in to comment.