Authorization
Authentication answers "who are you?" — authorization answers "what can you do?" This chapter covers patterns for controlling access based on user roles and permissions.
Role-based security functions
The simplest approach is checking roles in your security function:
-module(blog_auth).
-export([session_auth/1, admin_auth/1]).
session_auth(Req) ->
case nova_session:get(Req, <<"user_id">>) of
{ok, UserId} ->
{ok, User} = blog_repo:get(user, UserId),
{true, User};
{error, _} ->
{redirect, "/login"}
end.
admin_auth(Req) ->
case session_auth(Req) of
{true, #{role := admin} = User} ->
{true, User};
{true, _User} ->
{false, 403, #{}, #{error => <<"forbidden">>}};
Other ->
Other
end.
Use different security functions for different route groups:
routes(_Environment) ->
[
#{prefix => "",
security => fun blog_auth:session_auth/1,
routes => [
{"/", fun blog_main_controller:index/1, #{methods => [get]}}
]
},
#{prefix => "/admin",
security => fun blog_auth:admin_auth/1,
routes => [
{"/dashboard", fun blog_admin_controller:index/1, #{methods => [get]}}
]
}
].
Resource-level authorization
Sometimes you need to check ownership — "can this user edit this post?" This happens in the controller:
update(#{bindings := #{<<"id">> := Id}, json := Params,
auth_data := #{id := UserId}}) ->
case blog_repo:get(post, binary_to_integer(Id)) of
{ok, #{user_id := UserId} = Post} ->
%% User owns this post — allow update
CS = post:changeset(Post, Params),
case blog_repo:update(CS) of
{ok, Updated} -> {json, post_to_json(Updated)};
{error, CS1} -> {json, 422, #{}, #{errors => changeset_errors_to_json(CS1)}}
end;
{ok, _Post} ->
%% Different user — forbidden
{status, 403, #{}, #{error => <<"you can only edit your own posts">>}};
{error, not_found} ->
{status, 404, #{}, #{error => <<"post not found">>}}
end.
Token-based API authentication
For APIs, use bearer tokens instead of sessions:
api_auth(Req) ->
case cowboy_req:header(<<"authorization">>, Req) of
<<"Bearer ", Token/binary>> ->
case blog_accounts:verify_token(Token) of
{ok, User} -> {true, User};
{error, _} -> {false, 401, #{}, #{error => <<"invalid token">>}}
end;
_ ->
{false, 401, #{}, #{error => <<"missing authorization header">>}}
end.
Combining authentication strategies
Different route groups can use different strategies:
routes(_Environment) ->
[
%% Public
#{prefix => "", security => false,
routes => [
{"/login", fun blog_main_controller:login/1, #{methods => [get, post]}}
]},
%% Session-based (HTML pages)
#{prefix => "", security => fun blog_auth:session_auth/1,
routes => [
{"/", fun blog_main_controller:index/1, #{methods => [get]}},
{"/logout", fun blog_main_controller:logout/1, #{methods => [get]}}
]},
%% Token-based (API)
#{prefix => "/api", security => fun blog_auth:api_auth/1,
routes => [
{"/posts", fun blog_posts_controller:list/1, #{methods => [get]}},
{"/posts", fun blog_posts_controller:create/1, #{methods => [post]}}
]}
].
With authentication and authorization covered, let's move to the visual layer. Next: ErlyDTL Templates.