The request side of Macaw.Auth, for a Phoenix router.
import Macaw.Auth.Plug
pipeline :browser do
plug :fetch_session
plug :fetch_flash
plug Macaw.Plug.ClientIp, trusted_proxies: [...]
plug :fetch_current_user
end
pipeline :signed_in do
plug :require_authenticated
endfetch_current_user/2 assigns :current_user, :current_session and
:pending_step (all nil when signed out). require_authenticated/2 sends
a signed-out visitor to the sign-in page (remembering where a GET was
going) and a session that owes a step to that step's page; only the
step's page and sign-out are reachable meanwhile.
The second factor's page calls verify_second_factor/2 with the
submitted code; enrollment's page, after the backend confirms it,
complete_step(conn, :second_factor_enroll).
Run Macaw.Plug.ClientIp before these, so a session records, and the
throttle counts, the client's address, not a proxy's. Run fetch_flash
before require_role/2 and authorize/2, or their refusal goes home
without saying why.
Remembering a browser. log_in(conn, user, remember: true) (a
"remember me" box) also sets a signed cookie holding the session's token
(remember_cookie/0, config :macaw_auth, remember_cookie:), which
fetch_current_user/2 reads when the session has none, after the
browser closed. It lives for the backend's idle limit, set again each
time the backend records the session's use, is http_only
and same_site: "Lax", and is secure exactly when the request is
https; no setting turns that off. It dies with its session: a revoked
or expired session's token no longer resolves, and log_out/1 deletes
it. Behind a TLS-terminating proxy, make the request's scheme the
client's (Plug.RewriteOn with :x_forwarded_proto), or the cookie goes
out without secure. The session cookie is the host's (its endpoint's
Plug.Session options); set secure: true there for an https site, as
this cookie does by itself.
LiveView sockets. log_in/3 puts the session's live_socket_id
(Macaw.Auth.live_socket_id/1) in the session, so Phoenix names the
session's LiveView sockets by it. With config :macaw_auth, endpoint: MyAppWeb.Endpoint, every revocation (Macaw.Auth.revoke/2, log_out/1)
broadcasts "disconnect" on it and Phoenix drops those sockets; without
:endpoint the hooks' own guard (Macaw.Auth.Live) takes the pages away.
Redirects are Phoenix.Controller.redirect/2's, behind this module's own
check that the target is a path on this site (site_path?/1). A
remembered path came from a request, and Phoenix raises on one that
isn't safe, so anything else goes home instead: it neither leaves the
site nor fails the request.
Summary
Functions
Checks credentials as Macaw.Auth.authenticate/2 does, passing the
client's address (conn.remote_ip, the client's after
Macaw.Plug.ClientIp) as :remote_ip, for the backend's throttle.
Asks the app's authorizer (Macaw.Auth.authorize/3) whether the current
user may do action, and refuses as require_role/2 does: home, with an
error flash when the pipeline ran fetch_flash. A signed-out visitor is
sent to sign in, as require_role/2 sends one, remembering where a GET
was going. With no authorizer configured, every request is refused.
Records a step the current session completed and sends it on: to the next step, else where it was going. A session that no longer exists (revoked meanwhile) is logged out.
Sends the browser to the session's next step, or where it was going.
Assigns the signed-in user, session and pending step, or nils.
Starts a session for a user who passed Macaw.Auth.authenticate/2: the
old session is renewed (no fixation), the token stored with the
session's live_socket_id (Macaw.Auth.live_socket_id/1), and the
browser sent to the step the session owes, else where it was going,
else home.
Revokes the session (telling its open pages, and with :endpoint
disconnecting its LiveView sockets), renews the session, deletes the
remember cookie, and goes to sign in.
Sends a signed-in visitor away from the sign-in page.
The remember cookie's name: config :macaw_auth, remember_cookie:,
default "_macaw_auth_remember".
Sends a signed-out visitor to sign in, and a session owing a step to that step's page.
Sends a user with none of roles home, with an error flash when the
pipeline ran fetch_flash (without it, home with no message). A
signed-out visitor is sent to sign in instead, remembering where a GET
was going, as require_authenticated/2 does. What a role allows is the
app's to decide; this only checks membership.
Whether path is a path on this site that Phoenix.Controller.redirect/2
accepts: one leading slash, then none of a backslash, a tab or a line
break.
The session key holding the token, for hosts that need it (LiveView's session).
Checks a second-factor code for the current session, passing the
client's address for the backend's throttle. On success the
:second_factor step is complete and the browser goes on, to the next
step or where it was going: {:ok, conn} (redirected, halted). On a
refusal the conn stays for the app's page to render the reason:
{:error, reason, conn} (:invalid_code, :throttled, or the
session's own refusal), and with no current session
{:error, :invalid, conn}.
Functions
Checks credentials as Macaw.Auth.authenticate/2 does, passing the
client's address (conn.remote_ip, the client's after
Macaw.Plug.ClientIp) as :remote_ip, for the backend's throttle.
Asks the app's authorizer (Macaw.Auth.authorize/3) whether the current
user may do action, and refuses as require_role/2 does: home, with an
error flash when the pipeline ran fetch_flash. A signed-out visitor is
sent to sign in, as require_role/2 sends one, remembering where a GET
was going. With no authorizer configured, every request is refused.
plug :authorize, :manage
plug :authorize, {:edit, resource: &MyAppWeb.Posts.post/1}The resource is what the :resource option's function (a remote capture,
&Mod.fun/1, so the plug's options compile) gives of the conn, else nil.
It is asked only of a signed-in request.
Records a step the current session completed and sends it on: to the next step, else where it was going. A session that no longer exists (revoked meanwhile) is logged out.
Sends the browser to the session's next step, or where it was going.
Assigns the signed-in user, session and pending step, or nils.
The token is the session's; only when the session has none is it read
from the remember cookie, and then, if it resolves, put back into the
session (renewed, with the session's live_socket_id) and the cookie
re-issued for another idle limit. A refused token is deleted from the
session, and a refused remembered one from the cookie too.
When the backend records the session's use on this request (its touch, at most its interval), a remember cookie the browser sent for this session is set again too, for a fresh idle limit: a browser kept open past the idle limit keeps its cookie as long as its session lives. A browser that sent none never gets one here.
Starts a session for a user who passed Macaw.Auth.authenticate/2: the
old session is renewed (no fixation), the token stored with the
session's live_socket_id (Macaw.Auth.live_socket_id/1), and the
browser sent to the step the session owes, else where it was going,
else home.
remember: true also sets the remember cookie (remember_cookie/0):
signed, holding the token, for the backend's idle limit
(Macaw.Auth.session_idle_seconds/1), http_only, same_site: "Lax",
and secure exactly when the request is https. Without it, a remember
cookie the browser sent is deleted. Other opts go to
Macaw.Auth.create_session/2.
When the backend refuses the session ({:error, :invalid}: the user no
longer exists), the sign-in fails: back to the sign-in page, with an
error flash when the pipeline ran fetch_flash, and neither the session
nor any cookie touched.
Revokes the session (telling its open pages, and with :endpoint
disconnecting its LiveView sockets), renews the session, deletes the
remember cookie, and goes to sign in.
Sends a signed-in visitor away from the sign-in page.
@spec remember_cookie() :: String.t()
The remember cookie's name: config :macaw_auth, remember_cookie:,
default "_macaw_auth_remember".
Sends a signed-out visitor to sign in, and a session owing a step to that step's page.
Sends a user with none of roles home, with an error flash when the
pipeline ran fetch_flash (without it, home with no message). A
signed-out visitor is sent to sign in instead, remembering where a GET
was going, as require_authenticated/2 does. What a role allows is the
app's to decide; this only checks membership.
Whether path is a path on this site that Phoenix.Controller.redirect/2
accepts: one leading slash, then none of a backslash, a tab or a line
break.
The session key holding the token, for hosts that need it (LiveView's session).
@spec verify_second_factor(Plug.Conn.t(), term()) :: {:ok, Plug.Conn.t()} | {:error, atom(), Plug.Conn.t()}
Checks a second-factor code for the current session, passing the
client's address for the backend's throttle. On success the
:second_factor step is complete and the browser goes on, to the next
step or where it was going: {:ok, conn} (redirected, halted). On a
refusal the conn stays for the app's page to render the reason:
{:error, reason, conn} (:invalid_code, :throttled, or the
session's own refusal), and with no current session
{:error, :invalid, conn}.
def create(conn, %{"code" => code}) do
case Macaw.Auth.Plug.verify_second_factor(conn, code) do
{:ok, conn} -> conn
{:error, reason, conn} -> render(conn, :new, error: reason)
end
end