PhoenixApiToolkit.Security.Plugs (Phoenix API Toolkit v2.1.1) 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

Link to this function

ajax_csrf_protect(conn, arg2 \\ nil)

View Source

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
Link to this function

put_security_headers(conn, opts \\ [])

View Source

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"}]
Link to this function

require_content_type(conn, arg2 \\ nil)

View Source

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
Link to this function

set_forwarded_ip(conn, opts \\ [])

View Source

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}
Link to this function

verify_oauth2_aud(conn, exp_aud)

View Source

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
Link to this function

verify_oauth2_scope(conn, exp_scopes)

View Source

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