% % Copyright (c) 2024 Andrea Biscuola % % Permission to use, copy, modify, and distribute this software for any % purpose with or without fee is hereby granted, provided that the above % copyright notice and this permission notice appear in all copies. % % THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES % WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF % MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR % ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES % WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN % ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF % OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. % -module(dudeswave_auth). -moduledoc """ Dudes users management module. Here lives all the functions for the APIs needed to create, update and delete users from the dudeswave database. """. -include_lib("dudeswave/include/defines.hrl"). -include_lib("storage/include/storage.hrl"). -export([authenticate/2, details/1, new/3, update/4, delete/1, logout/2, auth_cookies/1, invalidate_cookies/1, set_auth_cookies/4, read_login_data/1, read_new_user_data/1, read_update_user_data/1]). -doc """ Verify a session with an existing cookie. Spec: ``` -spec authenticate(User, Auth) -> true | false | {true, Cookie, Validity} | {error, Reason} when User :: binary(), Auth :: {cookie, binary()} | {password, binary()}, Reason :: term(). ``` Authenticate a user with either an existing `Cookie` or by issuing a new one after authenticating with `Password`. If `Cookie` is valid, the function returns `true`. If the authentication is denied returns `false` """. -spec authenticate(User, Auth) -> true | false | {true, Cookie, Validity} | {error, Reason} when User :: binary(), Auth :: {cookie, binary()} | {password, binary()}, Cookie :: binary(), Validity :: pos_integer(), Reason :: term(). authenticate(User, {cookie, Cookie}) -> case storage:read(?COOKIESBUCK, Cookie) of {ok, [R]} -> CurTime = calendar:now_to_universal_time(erlang:timestamp()), CookieTime = R#object.value, CookieUser = lists:keyfind(user, 1, R#object.metadata), if CookieTime >= CurTime -> if User =:= CookieUser -> true; true -> false end; true -> false end; {ok, []} -> false; {error, _} -> {error, service_unavailable} end; authenticate(User, {password, Password}) -> case storage:read(?USERSBUCK, User) of {ok, [R]} -> Validity = case application:get_env(cookie_validity) of {ok, Value} -> erlang:system_time(seconds) + Value * 86400; undefined -> erlang:system_time(seconds) + ?DEFVALIDITY * 86400 end, {ok, Hash} = lists:keyfind(hash, 1, R#object.metadata), {ok, Salt} = lists:keyfind(salt, 1, R#object.metadata), Auth = crypto:hash(sha256, <>), if Auth =:= Hash -> Cookie = base64:encode(rand:bytes(64)), case storage:write(?COOKIESBUCK, <>, Validity, [{user, User}]) of ok -> {true, Cookie, Validity}; {error, Reason} -> {error, Reason} end; true -> false end; {ok, []} -> false; {error, Reason} -> {error, Reason} end. -doc """ Close an existing session Spec: ``` -spec logout(User, Cookie) -> ok | {error, Reason} when User :: binary(), Cookie :: binary(), Reason :: term(). ``` Invalidate and delete `Cookie` associated with `User` from the system. """. -spec logout(User, Cookie) -> ok | {error, Reason} when User :: binary(), Cookie :: binary(), Reason :: term(). logout(User, Cookie) -> case storage:read(?COOKIESBUCK, Cookie) of {ok, [R]} -> {user, User} = lists:keyfind(user, 1, R#object.metadata), storage:delete(?COOKIESBUCK, Cookie); {ok, []} -> {error, not_found}; {error, Reason} -> {error, Reason} end. -doc """ Return user details. Spec: ``` -spec details(User) -> Value | {error, Reason} when User :: binary(), Value :: term(), Reason :: term(). ``` """. -spec details(User) -> Value | {error, Reason} when User :: binary(), Value :: term(), Reason :: term(). details(User) -> case storage:read(?USERSBUCK, User) of {ok, [R]} -> R#object.value; {error, Reason} -> {error, Reason}; {ok, []} -> {error, not_found} end. -doc """ Create a new user. Spec: ``` -spec new(User, Password, Email) -> ok | {error, Reason} when User :: binary(), Password :: binary(), Email :: binary(), Reason :: term(). ``` The `User` is created, and stored in the application's users bucket `Password` is salted and hashed with SHA256 before being stored. The new user is saved with a metadata `status` of `waiting_confirmation`, based on the application settings, the confirmation method may vary. """. -spec new(User, Password, Email) -> ok | {error, Reason} when User :: binary(), Password :: binary(), Email :: binary(), Reason :: term(). new(User, Password, Email) -> Salt = rand:bytes(?RANDBYTES), Hash = crypto:hash(sha256, <>), Data = #{<<"email">> => Email}, Metadata = [{salt, Salt}, {hash, Hash}, {status, waiting_confirmation}], storage:write(?USERSBUCK, User, Data, Metadata). -doc """ Update user's details Spec: ``` -spec update(User, Name, Email, Desc) -> ok | {error, Reason} when User :: binary(), Name :: binary(), Email :: binary(), Desc :: binary(), Reason :: term(). ``` The details apart from `User` are updated. The username itself is immutable and cannot be modified. All the other fields, excluding the e-mail, are the ones that can be seen in the public page. """. -spec update(User, Name, Email, Desc) -> ok | {error, Reason} when User :: binary(), Name :: binary(), Email :: binary(), Desc :: binary(), Reason :: term(). update(User, Name, Email, Desc) -> {ok, CurData, Metadata} = case storage:read(?USERSBUCK, User) of {ok, [R]} -> {ok, R#object.value, R#object.metadata}; {error, Reason} -> {error, Reason} end, Data = CurData#{<<"email">> => Email, <<"name">> => Name, <<"description">> => Desc}, storage:write(?USERSBUCK, User, Data, Metadata). -doc """ Delete an existing user from the database. ``` -spec delete(User) -> ok | {error, Reason} when User :: binary(), Reason :: term(). ``` """. -spec delete(User) -> ok | {error, Reason} when User :: binary(), Reason :: term(). delete(User) -> % We are missing the cleanup of the cookies % here. For that, we need to add at least another % API to the storage layer. storage:delete(?USERSBUCK, User). -doc """ Get the authentication cookies from a cowboy request. Spec: ``` -spec auth_cookies(Req) -> {User, Cookie} when Req :: cowboy_req:req(), User :: binary(), Cookie :: binary(). ``` """. -spec auth_cookies(Req) -> {User, Cookie} when Req :: cowboy_req:req(), User :: binary(), Cookie :: binary(). auth_cookies(Req) -> #{?DUDEAUTH := Cookie, ?DUDENAME := User} = cowboy_req:match_cookies([?DUDEAUTH, ?DUDENAME], Req), {User, Cookie}. -doc """ Invalidate the cookies in the passed request. Spec: ``` -spec invalidate_cookies(Req) -> Req0 when Req :: cowboy_req:req(), Req0 :: cowboy_req:req(). ``` A new request `Req0` is returned to the caller with the cookies zeroed and completely invalidated. """. -spec invalidate_cookies(Req) -> Req0 when Req :: cowboy_req:req(), Req0 :: cowboy_req:req(). invalidate_cookies(Req) -> Req0 = cowboy_req:set_resp_cookie(<<"?DUDEAUTH">>, <<"">>, Req, #{max_age => 0}), Req1 = cowboy_req:set_resp_cookie(<<"?DUDENAME">>, <<"">>, Req0, #{max_age => 0}), Req1. -doc """ Set the authentication cookies for the provided clien request Spec: ``` -spec set_auth_cookies(Req, User, Cookie, Validity) -> Req0 when Req :: cowboy_req:req(), User :: binary(), Cookie :: binary(), Validity :: pos_integer(), Req0 :: cowboy_req:req(). ``` A new request object `Req0`is returned, with the user and auth cookies set. """. -spec set_auth_cookies(Req, User, Cookie, Validity) -> Req0 when Req :: cowboy_req:req(), User :: binary(), Cookie :: binary(), Validity :: pos_integer(), Req0 :: cowboy_req:req(). set_auth_cookies(Req, User, Cookie, Validity) -> Req0 = cowboy_req:set_resp_cookie(<<"?DUDEAUTH">>, Cookie, Req, #{max_age => Validity}), Req1 = cowboy_req:set_resp_cookie(<<"?DUDENAME">>, User, Req0, #{max_age => Validity}), Req1. -doc """ Spec: ``` -spec read_login_data(Req) -> {User, Pass, Req0} when Req :: cowboy_req:req(), User :: binary(), Pass :: binary(), Req0 :: cowboy_req:req(). ``` Read the login details from the `Req` body and return `User` and `Password`. """. -spec read_login_data(Req) -> {User, Pass, Req0} when Req :: cowboy_req:req(), User :: binary(), Pass :: binary(), Req0 :: cowboy_req:req(). read_login_data(Req) -> {ok, Data, Req0} = cowboy_req:read_body(Req), #{<<"user">> := User, <<"password">> := Pass} = json:decode(Data), {User, Pass, Req0}. -doc """ Read new registration informations from the request Spec: ``` -spec read_new_user_data(Req) -> {User, Pass, Email Req0} when Req :: cowboy_req:req(), User :: binary(), Pass :: binary(), Email :: binary(), Req0 :: cowboy_req:req(). ``` """. -spec read_new_user_data(Req) -> {User, Pass, Email, Req0} when Req :: cowboy_req:req(), User :: binary(), Pass :: binary(), Email :: binary(), Req0 :: cowboy_req:req(). read_new_user_data(Req) -> {ok, Data, Req0} = cowboy_req:read_body(Req), #{<<"user">> := User, <<"password">> := Pass, <<"email">> := Email} = json:decode(Data), {User, Pass, Email, Req0}. -doc """ Update user informations. Spec: ``` -spec read_update_user_data(Req) -> {Email, Desc, Name, Req0} when Req :: cowboy_req:req(), Email :: binary(), Desc :: binary(), Name :: binary(), Req0 :: cowboy_req:req(). ``` """. -spec read_update_user_data(Req) -> {Email, Desc, Name, Req0} when Req :: cowboy_req:req(), Email :: binary(), Desc :: binary(), Name :: binary(), Req0 :: cowboy_req:req(). read_update_user_data(Req) -> {ok, Data, Req0} = cowboy_req:read_body(Req), #{<<"email">> := Email, <<"description">> := Desc, <<"name">> := Name} = json:decode(Data), {Email, Desc, Name, Req0}.