Compare commits

..

No commits in common. "dcc7bc3ca3fe55bf1ab261441d1dd5df6b47f6db" and "df09a192b0018964a9d4a2c2b4f431bbe30c5b65" have entirely different histories.

6 changed files with 31 additions and 291 deletions

View File

@ -3,7 +3,7 @@
{vsn,"1.0.0"}, {vsn,"1.0.0"},
{modules,[dudeswave,dudeswave_app,dudeswave_handler, {modules,[dudeswave,dudeswave_app,dudeswave_handler,
dudeswave_user_handler,dudeswave_supervisor, dudeswave_user_handler,dudeswave_supervisor,
dudeswave_auth,dudeswave_auth_handler]}, dudeswave_auth]},
{registered,[]}, {registered,[]},
{applications,[kernel,stdlib,erts,cowboy,ranch]}, {applications,[kernel,stdlib,erts,cowboy,ranch]},
{mod,{dudeswave_app,[]}}, {mod,{dudeswave_app,[]}},

View File

@ -6,7 +6,6 @@ ERLC?= erlc -server
OBJS= dudeswave.beam dudeswave_app.beam OBJS= dudeswave.beam dudeswave_app.beam
OBJS+= dudeswave_supervisor.beam dudeswave_handler.beam OBJS+= dudeswave_supervisor.beam dudeswave_handler.beam
OBJS+= dudeswave_user_handler.beam dudeswave_auth.beam OBJS+= dudeswave_user_handler.beam dudeswave_auth.beam
OBJS+= dudeswave_auth_handler.beam
all: ${OBJS} all: ${OBJS}

View File

@ -40,7 +40,7 @@ start(_Type, StartArgs) ->
Dispatch = cowboy_router:compile([ Dispatch = cowboy_router:compile([
{'_', [ {'_', [
{"/api/v1/user", dudeswave_user_handler, #{bucket => ?USERSBUCK, {"/user/:username", dudeswave_user_handler, #{bucket => ?USERSBUCK,
cookies => ?COOKIESBUCK}}, cookies => ?COOKIESBUCK}},
{"/", dudeswave_handler, #{bucket => ?APPBUCK}} {"/", dudeswave_handler, #{bucket => ?APPBUCK}}
]} ]}

View File

@ -27,7 +27,7 @@ from the dudeswave database.
-include_lib("storage/include/storage.hrl"). -include_lib("storage/include/storage.hrl").
-export([authenticate/3, authenticate/4, details/2, new/4, -export([authenticate/3, authenticate/4, details/2, new/4,
update/5, delete/2, logout/3]). update/5, delete/2]).
-doc """ -doc """
Verify a session with an existing cookie. Verify a session with an existing cookie.
@ -55,13 +55,11 @@ authenticate(User, Cookie, Bucket) ->
case storage:read(Bucket, Cookie) of case storage:read(Bucket, Cookie) of
{ok, [R]} -> {ok, [R]} ->
CurTime = calendar:now_to_universal_time(erlang:timestamp()), CurTime = calendar:now_to_universal_time(erlang:timestamp()),
CookieTime = R#object.value, CookieTime = lists:keyfind(until, 1, R#object.metadata),
CookieUser = lists:keyfind(user, 1, R#object.metadata),
if if
CookieTime >= CurTime -> CookieTime >= CurTime ->
if if
User =:= CookieUser -> true; User =:= R#object.value -> true;
true -> false true -> false
end; end;
true -> false true -> false
@ -120,7 +118,7 @@ authenticate(User, Password, Cookies, Bucket) ->
if if
Auth =:= Hash -> Auth =:= Hash ->
Cookie = base64:encode(rand:bytes(64)), Cookie = base64:encode(rand:bytes(64)),
case storage:write(Cookies, <<Cookie/binary>>, Validity, [{user, User}]) of case storage:write(Cookies, <<Cookie/binary>>, User, [{until, Validity}]) of
ok -> {true, Cookie, Validity}; ok -> {true, Cookie, Validity};
{error, Reason} -> {error, Reason} {error, Reason} -> {error, Reason}
end; end;
@ -130,38 +128,6 @@ authenticate(User, Password, Cookies, Bucket) ->
{error, Reason} -> {error, Reason} {error, Reason} -> {error, Reason}
end. 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 """ -doc """
Return user details. Return user details.

View File

@ -1,228 +0,0 @@
%
% Copyright (c) 2024 Andrea Biscuola <a@abiscuola.com>
%
% 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_handler).
-moduledoc """
JSON API to authenticate users.
```
/api/v1/auth
```
Cookies used in this module are:
```
dudename: The username.
dudeauth: Authentication cookie.
This module accepts four methods:
- POST /api/v1/auth
Authenticate a user with it's password. If successful, set the new
cookies with the authentication details in the browser.
- DELETE /api/v1/auth
Logout the user from the current session and invalidate all the
authentication cookies, if present.
If an operation fails, the response JSON is in the form:
```
{
"error": "error string"
}
```
JSON APIs
POST /api/v1/auth
```
{
"user": "foo",
"password": "SecurePassword123",
}
```
Response codes:
- 200 OK
- 400 Bad Request
- 404 Not Found
Response body:
If authentication successful:
```
{
"result": "ok"
}
```
DELETE /api/v1/auth
- 202 Accepted
- 404 Not Found
If operation successful;
```
{
"result": "deleted"
}
```
""".
-behaviour(cowboy_handler).
-export([init/2, terminate/3]).
%
% Callbacks exports
%
-export([allowed_methods/2, content_types_accepted/2,
known_methods/2, is_authorized/2, forbidden/2,
resource_exists/2, is_conflict/2, previously_existed/2,
allow_missing_post/2, delete_resource/2, delete_completed/2,
login/2, logout/2]).
%
% Cowboy standard callbacks
%
init(Req, State) ->
{cowboy_rest, Req, State}.
known_methods(Req, State) ->
{[<<"POST">>, <<"DELETE">>], Req, State}.
allowed_methods(Req, State) ->
{[<<"POST">>, <<"DELETE">>], Req, State}.
is_authorized(Req, State) -> {true, Req, State}.
forbidden(Req, State) ->
case cowboy_req:method(Req) of
<<"POST">> ->
{false, Req, State};
_ ->
#{dudeauth := Auth, dudename := User} = cowboy_req:match_cookies([dudeauth,
dudename], Req),
{ok, Bucket} = maps:find(cookies, State),
case dudeswave_auth:authenticate(User, Auth, Bucket) of
{error, service_unavailable} -> exit(service_unavailable);
true -> {false, Req, State};
false ->
Resp = json:encode(#{<<"error">> => <<"authentication required">>}),
Req0 = cowboy_req:reply(403, #{}, Resp, Req),
{true, Req0, State}
end
end.
content_types_accepted(Req, State) ->
case cowboy_req:method(Req) of
<<"POST">> ->
{[{<<"application/json">>, login}], Req, State};
<<"DELETE">> ->
{[{<<"application/json">>, logout}], Req, State}
end.
resource_exists(Req, State) ->
#{dudename := User} = cowboy_req:match_cookies([dudename], Req),
{ok, Bucket} = maps:find(bucket, State),
case dudeswave_auth:user_details(User, Bucket) of
[] ->
Resp = json:encode(#{<<"error">> => <<"user does not exists">>}),
Req0 = cowboy_req:reply(404, #{}, Resp, Req),
{false, Req0, State};
{error, Reason} -> exit(Reason);
_ ->
NewState = State#{
bucket => Bucket,
user_exists => true
},
{true, Req, NewState}
end.
previously_existed(Req, State) -> {false, Req, State}.
is_conflict(Req, #{user_exists := true}) ->
{false, Req, []};
is_conflict(Req, State) -> {true, Req, State}.
allow_missing_post(Req, State) -> {false, Req, State}.
delete_resource(Req, State) ->
{ok, Bucket} = maps:find(bucket, State),
#{dudename := User, dudeauth := Auth} = cowboy_req:match_cookies([dudename,
dudeauth], Req),
case dudeswave_auth:logout(User, Auth, Bucket) of
ok ->
Req0 = cowboy_req:set_resp_cookie(<<"dudeauth">>, Auth, Req,
#{max_age => 0}),
Req1 = cowboy_req:set_resp_cookie(<<"dudename">>, User, Req0,
#{max_age => 0}),
Resp = json:encode(#{<<"result">> => <<"deleted">>}),
Req2 = cowboy_req:reply(200, #{}, Resp, Req1),
{true, Req2, State};
{error, _} -> {false, Req, State}
end.
delete_completed(Req, State) -> {false, Req, State}.
%
% Custom callbacks
%
login(Req, State) ->
{ok, Cookies} = maps:find(State, cookies),
{ok, Bucket} = maps:find(State, bucket),
{ok, Data, Req0} = cowboy_req:read_body(Req),
#{<<"user">> := User, <<"password">> := Pass} = json:decode(Data),
case dudeswave_auth:authenticate(User, Pass, Cookies, Bucket) of
{true, Cookie, Validity} ->
Resp = json:encode(#{<<"result">> => <<"ok">>}),
Req1 = cowboy_req:set_resp_cookie(<<"dudeauth">>, Cookie, Req0,
#{max_age => Validity}),
Req2 = cowboy_req:set_resp_cookie(<<"dudename">>, User, Req1,
#{max_age => Validity}),
Req3 = cowboy_req:reply(200, #{}, Resp, Req2),
{true, Req3, State};
false ->
Resp = json:encode(#{<<"error">> => <<"authentication failed">>}),
Req1 = cowboy_req:reply(401, #{}, Resp, Req0),
{false, Req1, State};
{error, _} ->
Resp = json:encode(#{<<"error">> => <<"internal error">>}),
Req1 = cowboy_req:reply(500, #{}, Resp, Req0),
{false, Req1, State}
end.
% Provided but not used
logout(Req, State) -> {ok, Req, State}.
%
% gen_server callbacks
%
terminate(_Reason, _Req, _State) -> ok.

View File

@ -17,28 +17,33 @@
-moduledoc """ -moduledoc """
JSON API to manage users. JSON API to manage users.
The username is passed in a cookie. The handler recover it from the The username should be passed as one of the tokens of the request path, like
session. Cookies are:
``` ```
dudename # the actual username /user/foo
dudeauth # the authentication cookie /user/foo/details
``` ```
However, the first form is preferred.
The user parameter must be called `username` as this module expects it
in order to work properly. All the requests must be done with a valid
session cookie in order to work.
If the session is not valid, all the requests will return `403 Forbidden` to If the session is not valid, all the requests will return `403 Forbidden` to
the client. In case a technical problem occurs, `500 Internal Server Error` the client. In case a technical problem occurs, `500 Internal Server Error`
is returned. is returned.
This module accepts four methods: This module accepts four methods:
- GET /api/v1/user - GET /user/:username
Retrieve user's details. However, this call requires the user to have Retrieve user's details. However, this call requires the user to have
a valid cookie set. Not suitable for a public page. a valid cookie set. Not suitable for a public page.
- POST /api/v1/user - POST /user/:username
Update user's details, like their name, description and whatnot. Update user's details, like their name, description and whatnot.
- DELETE /api/v1/user - DELETE /user/:username
Remove a user forever. The data is delete immediately. However, Remove a user forever. The data is delete immediately. However,
it's content is left up there. Probably a specific option will be added it's content is left up there. Probably a specific option will be added
later. This request does not have a body. The call deletes the user later. This request does not have a body. The call deletes the user
@ -46,7 +51,7 @@ This module accepts four methods:
for the simple reason that we may make the call asynchronous for the simple reason that we may make the call asynchronous
to remove additional content in background. to remove additional content in background.
- PUT /api/v1/user - PUT /user/:username
Register a user. The registration takes only three parameter: username, Register a user. The registration takes only three parameter: username,
password and e-mail. The e-mail is required if a confirmation message password and e-mail. The e-mail is required if a confirmation message
is to be sent. The plan is to have a separate process handle this, so the is to be sent. The plan is to have a separate process handle this, so the
@ -54,11 +59,10 @@ This module accepts four methods:
JSON APIs JSON APIs
GET /api/v1/user GET /user/:username
``` ```
{ {
"user" : "foo",
"email": "foo@example.com", "email": "foo@example.com",
"description": "A wonderful user", "description": "A wonderful user",
"name": "Fantastic Foo" "name": "Fantastic Foo"
@ -70,7 +74,7 @@ Response codes:
- 200 OK (Success) - 200 OK (Success)
- 404 Not Found - 404 Not Found
PUT /api/v1/user PUT /user/:username
``` ```
{ {
@ -85,7 +89,7 @@ Response codes:
- 400 Bad Request - 400 Bad Request
- 409 Conflict (User already exists) - 409 Conflict (User already exists)
POST /api/v1/user POST /user/:username
``` ```
{ {
@ -101,11 +105,12 @@ Response codes:
- 400 Bad Request - 400 Bad Request
- 404 Not Found - 404 Not Found
DELETE /api/v1/user DELETE /user/:username
- 202 Accepted - 202 Accepted
- 404 Not Found - 404 Not Found
""". """.
-behaviour(cowboy_handler). -behaviour(cowboy_handler).
@ -141,9 +146,9 @@ forbidden(Req, State) ->
<<"PUT">> -> <<"PUT">> ->
{false, Req, State}; {false, Req, State};
_ -> _ ->
#{dudeauth := Auth, dudename := User} = cowboy_req:match_cookies([dudeauth, #{dudeauth := Auth} = cowboy_req:match_cookies([dudeauth], Req),
dudename], Req),
{ok, Bucket} = maps:find(cookies, State), {ok, Bucket} = maps:find(cookies, State),
User = cowboy_req:binding(username, Req),
case dudeswave_auth:authenticate({cookie, User, Auth}, Bucket) of case dudeswave_auth:authenticate({cookie, User, Auth}, Bucket) of
{error, service_unavailable} -> exit(service_unavailable); {error, service_unavailable} -> exit(service_unavailable);
@ -173,7 +178,7 @@ content_types_accepted(Req, State) ->
end. end.
resource_exists(Req, State) -> resource_exists(Req, State) ->
#{dudename := User} = cowboy_req:match_cookies([dudename], Req), User = cowboy_req:binding(username, Req),
{ok, Bucket} = maps:find(bucket, State), {ok, Bucket} = maps:find(bucket, State),
case dudeswave_auth:user_details(User, Bucket) of case dudeswave_auth:user_details(User, Bucket) of
@ -200,7 +205,7 @@ allow_missing_post(Req, State) -> {false, Req, State}.
delete_resource(Req, State) -> delete_resource(Req, State) ->
{ok, Bucket} = maps:find(bucket, State), {ok, Bucket} = maps:find(bucket, State),
#{dudename := User} = cowboy_req:match_cookies([dudename], Req), User = cowboy_req:binding(username, Req),
case dudeswave_auth:delete(User, Bucket) of case dudeswave_auth:delete(User, Bucket) of
ok -> {true, Req, State}; ok -> {true, Req, State};
@ -215,7 +220,7 @@ delete_completed(Req, State) -> {false, Req, State}.
create_user(Req, State) -> create_user(Req, State) ->
{ok, Bucket} = maps:find(bucket, State), {ok, Bucket} = maps:find(bucket, State),
#{dudename := User} = cowboy_req:match_cookies([dudename], Req), User = cowboy_req:binding(username, Req),
#{<<"password">> := Pass, <<"email">> := Email} = json:decode(cowboy_req:body(req)), #{<<"password">> := Pass, <<"email">> := Email} = json:decode(cowboy_req:body(req)),
@ -226,7 +231,7 @@ create_user(Req, State) ->
modify_user(Req, State) -> modify_user(Req, State) ->
{ok, Bucket} = maps:find(bucket, State), {ok, Bucket} = maps:find(bucket, State),
#{dudename := User} = cowboy_req:match_cookies([dudename], Req), User = cowboy_req:binding(username, Req),
#{<<"email">> := Email, <<"description">> := Desc, #{<<"email">> := Email, <<"description">> := Desc,
<<"name">> := Name} = json:decode(cowboy_req:body(req)), <<"name">> := Name} = json:decode(cowboy_req:body(req)),
@ -238,10 +243,8 @@ modify_user(Req, State) ->
user_details(Req, State) -> user_details(Req, State) ->
#{details := Details} = State, #{details := Details} = State,
#{dudename := User} = cowboy_req:match_cookies([dudename], Req),
Data = Details#{user => User},
{iolist_to_binary(json:encode(Data)), Req, State}. {iolist_to_binary(json:encode(Details)), Req, State}.
% %
% gen_server callbacks % gen_server callbacks