PhoenixApiToolkit.Security.Plugs (Phoenix API Toolkit v3.0.0) View Source
Security-related plugs.
Several of these plugs are based on recommendations for API's by the OWASP guidelines.
Link to this section Summary
Functions
Protect AJAX-requests / API endpoints (ONLY those requests, not HTML forms!) against CSRF-attacks by requiring header x-csrf-token to be set to any value.
Adds security headers to the response as recommended for API's by OWASP. Sets
"x-frame-options": "deny" and "x-content-type-options": "nosniff".
Checks if the request's "content-type" header is present. Content matching is done by Plug.Parsers.
Set conn.remote_ip to the value in header "x-forwarded-for", if present.
Check if the JWT in conn.assigns.jwt has an "aud" claim that matches the exp_aud parameter.
This assign is set by PhoenixApiToolkit.Security.Oauth2Plug and should contain a JOSE.JWT struct.
Check if the JWT in conn.assigns.jwt has a "scope" claim that matches the exp_scopes parameter.
This assign is set by PhoenixApiToolkit.Security.Oauth2Plug and should contain a JOSE.JWT struct.
Link to this section Functions
Specs
ajax_csrf_protect(Plug.Conn.t(), any()) :: Plug.Conn.t()
Protect AJAX-requests / API endpoints (ONLY those requests, not HTML forms!) against CSRF-attacks by requiring header x-csrf-token to be set to any value.
This defense relies on the same-origin policy (SOP) restriction that only JavaScript can be used to add a custom header, and only within its origin. https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers
Examples / doctests
# requests that don't (shouldn't) change server state pass through
iex> conn(:get, "/") |> ajax_csrf_protect() |> Map.get(:halted)
false
# state-changing requests with the header pass through
iex> conn(:post, "/") |> put_req_header("x-csrf-token", "anything") |> ajax_csrf_protect() |> Map.get(:halted)
false
# state-changing requests without the header are rejected
iex> conn(:post, "/") |> ajax_csrf_protect()
** (PhoenixApiToolkit.Security.AjaxCSRFError) missing 'x-csrf-token' header
Specs
put_security_headers(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
Adds security headers to the response as recommended for API's by OWASP. Sets
"x-frame-options": "deny" and "x-content-type-options": "nosniff".
Examples
use Plug.Test
# it does what it says it does
iex> conn = conn(:get, "/")
iex> put_security_headers(conn).resp_headers -- conn.resp_headers
[{"x-frame-options", "deny"}, {"x-content-type-options", "nosniff"}]
Specs
require_content_type(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
Checks if the request's "content-type" header is present. Content matching is done by Plug.Parsers.
The filter is only applied to methods which are expected to carry contents, to PUT, POST and PATCH
methods, that is. Only one content-type header is allowed. A noncompliant request causes a
PhoenixApiToolkit.Security.MissingContentTypeError to be raised,
resulting in a 415 Unsupported Media Type response.
Examples
use Plug.Test
# safe methods pass through
iex> conn = conn(:get, "/")
iex> conn == require_content_type(conn)
true
# compliant unsafe methods (put, post and patch) pass through
iex> conn = conn(:post, "/") |> put_req_header("content-type", "application/json")
iex> conn == require_content_type(conn)
true
# noncompliant unsafe methods cause a MissingContentTypeError to be raised
iex> conn(:post, "/") |> require_content_type()
** (PhoenixApiToolkit.Security.MissingContentTypeError) missing 'content-type' header
Specs
set_forwarded_ip(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
Set conn.remote_ip to the value in header "x-forwarded-for", if present.
## Examples
use Plug.Test
def conn_with_ip, do: conn(:get, "/") |> Map.put(:remote_ip, {127, 0, 0, 12})
# by default, the value of `remote_ip` is left alone
iex> conn = conn_with_ip() |> set_forwarded_ip()
iex> conn.remote_ip
{127, 0, 0, 12}
# if header "x-forwarded-for" is set, remote ip is overwritten
iex> conn = conn_with_ip() |> put_req_header("x-forwarded-for", "10.0.0.1") |> set_forwarded_ip()
iex> conn.remote_ip
{10, 0, 0, 1}
Specs
verify_oauth2_aud(Plug.Conn.t(), binary()) :: Plug.Conn.t()
Check if the JWT in conn.assigns.jwt has an "aud" claim that matches the exp_aud parameter.
This assign is set by PhoenixApiToolkit.Security.Oauth2Plug and should contain a JOSE.JWT struct.
If not, a PhoenixApiToolkit.Security.Oauth2TokenVerificationError is raised,
resulting in a 401 Unauthorized response.
Examples
use Plug.Test
def conn_with_aud(aud), do: conn(:get, "/") |> assign(:jwt, %{fields: %{"aud", aud}})
# if aud matches, the conn is passed through
iex> conn = conn_with_aud("my resource server")
iex> conn == conn |> verify_oauth2_aud("my resource server")
true
# an error is raised if aud does not match
iex> conn_with_aud("my resource server") |> verify_oauth2_aud("another server")
** (PhoenixApiToolkit.Security.Oauth2TokenVerificationError) Oauth2 token invalid: aud mismatch
Specs
verify_oauth2_scope(Plug.Conn.t(), [binary()]) :: Plug.Conn.t()
Check if the JWT in conn.assigns.jwt has a "scope" claim that matches the exp_scopes parameter.
This assign is set by PhoenixApiToolkit.Security.Oauth2Plug and should contain a JOSE.JWT struct.
If not, a PhoenixApiToolkit.Security.Oauth2TokenVerificationError is raised,
resulting in a 401 Unauthorized response.
Examples
use Plug.Test
def conn_with_scope(scope), do: conn(:get, "/") |> assign(:jwt, %{fields: %{"scope", scope}})
# if there is a matching scope, the conn is passed through
iex> conn = conn_with_scope("admin read:phone")
iex> conn == conn |> verify_oauth2_scope(["admin"])
true
iex> conn == conn |> verify_oauth2_scope(["admin", "not:a:match"])
true
iex> conn == conn |> verify_oauth2_scope(["admin", "read:phone"])
true
# an error is raised if there is no matching scope
iex> conn_with_scope("admin read:phone") |> verify_oauth2_scope(["not:a:match"])
** (PhoenixApiToolkit.Security.Oauth2TokenVerificationError) Oauth2 token invalid: scope mismatch