% % 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 needed to create, update and delete users from the dudeswave database. """. -define(RANDBYTES, 32). -define(DEFVALIDITY, 365). -include_lib("storage/include/storage.hrl"). -export([authenticate/3, authenticate/4, details/2, new/4, update/5, delete/2, logout/3]). -doc """ Verify a session with an existing cookie. Spec: ``` -spec authenticate(User, Cookie, Bucket) -> true | false | {error, Reason} when User :: binary(), Cookie :: binary(), Bucket :: atom(), Reason :: term(). ``` Check against che `Bucket` table, containing the list of current cookies, if `Cookie` is still valid to authenticate `User` session. """. -spec authenticate(User, Cookie, Bucket) -> true | false | {error, Reason} when User :: binary(), Cookie :: binary(), Bucket :: atom(), Reason :: term(). authenticate(User, Cookie, Bucket) -> case storage:read(Bucket, 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. -doc """ Authenticate a user and return a new cookie for the new session. Spec: ``` -spec authenticate(User, Password, Cookies, Bucket) -> {true, Cookie, Validity} | false | {error, Reason} when User :: binary(), Password :: binary(), Cookies :: atom(), Bucket :: atom(), Cookie :: binary(), Validity :: non_neg_integer(), Reason :: term(). ``` Authenticate `User` with `Password`, reading the user's details from `Bucket`. If the authentication is successful, a new cookie is generated and stored in the `Cookies` bucket. The cookie is returned to the caller in a tuple `{true, Cookie}`, otherwise `false` is returned and the authentication is denied. """. -spec authenticate(User, Password, Cookies, Bucket) -> {true, Cookie, Validity} | false | {error, Reason} when User :: binary(), Password :: binary(), Cookies :: atom(), Bucket :: atom(), Cookie :: binary(), Validity :: non_neg_integer(), Reason :: term(). authenticate(User, Password, Cookies, Bucket) -> case storage:read(Bucket, 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(Cookies, <>, 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, Bucket) -> ok | {error, Reason} when User :: binary(), Cookie :: binary(), Bucket :: atom(), Reason :: term(). ``` Invalidate and delete `Cookie` associated with `User` from the system. """. -spec logout(User, Cookie, Bucket) -> ok | {error, Reason} when User :: binary(), Cookie :: binary(), Bucket :: atom(), Reason :: term(). logout(User, Cookie, Bucket) -> case storage:read(Bucket, Cookie) of {ok, [R]} -> {user, User} = lists:keyfind(user, 1, R#object.metadata), storage:delete(Bucket, Cookie); {ok, []} -> {error, not_found}; {error, Reason} -> {error, Reason} end. -doc """ Return user details. Spec: ``` -spec details(User, Bucket) -> Value | {error, Reason} when User :: binary(), Bucket :: atom(), Value :: term(), Reason :: term(). ``` """. -spec details(User, Bucket) -> Value | {error, Reason} when User :: binary(), Bucket :: atom(), Value :: term(), Reason :: term(). details(User, Bucket) -> case storage:read(Bucket, 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, Bucket) -> ok | {error, Reason} when User :: binary(), Password :: binary(), Email :: binary(), Bucket :: atom(), Reason :: term(). ``` The `User` is added to `Bucket`, that is, the users bucket for the application. `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, Bucket) -> ok | {error, Reason} when User :: binary(), Password :: binary(), Email :: binary(), Bucket :: atom(), Reason :: term(). new(User, Password, Email, Bucket) -> Salt = rand:bytes(?RANDBYTES), Hash = crypto:hash(sha256, <>), Data = #{<<"email">> => Email}, Metadata = [{salt, Salt}, {hash, Hash}, {status, waiting_confirmation}], storage:write(Bucket, User, Data, Metadata). -doc """ Update user's details Spec: ``` -spec update(User, Name, Email, Desc, Bucket) -> ok | {error, Reason} when User :: binary(), Name :: binary(), Email :: binary(), Desc :: binary(), Bucket :: atom(), 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, Bucket) -> ok | {error, Reason} when User :: binary(), Name :: binary(), Email :: binary(), Desc :: binary(), Bucket :: atom(), Reason :: term(). update(User, Name, Email, Desc, Bucket) -> {ok, CurData, Metadata} = case storage:read(Bucket, 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(Bucket, User, Data, Metadata). -doc """ Delete an existing user from the database. ``` -spec delete(User, Bucket) -> ok | {error, Reason} when User :: binary(), Bucket :: atom(), Reason :: term(). ``` """. -spec delete(User, Bucket) -> ok | {error, Reason} when User :: binary(), Bucket :: atom(), Reason :: term(). delete(User, Bucket) -> % 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(Bucket, User).