Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add require authorization plug #21

Merged
merged 4 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ defmodule SampleAppWeb.Endpoint do
@client_id Application.compile_env!(:sample_app, [:openid_credentials, :client_id])
@client_secret Application.compile_env!(:sample_app, [:openid_credentials, :client_secret])

# Ensure Authorization Token provided
plug Oidcc.Plug.RequireAuthorization

# Check Token via Introspection
plug Oidcc.Plug.IntrospectToken,
provider: SampleApp.GoogleOpenIdConfigurationProvider,
Expand Down
1 change: 1 addition & 0 deletions lib/oidcc/plug/extract_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ defmodule Oidcc.Plug.ExtractAuthorization do

plug Oidcc.Plug.ExtractAuthorization

plug Oidcc.Plug.RequireAuthorization, [...] # Ensure Authorization Token provided
plug Oidcc.Plug.IntrospectToken, [...] # Check Token via Introspection
plug Oidcc.Plug.LoadUserinfo, [...] # Check Token via Userinfo
plug Oidcc.Plug.ValidateJwtToken, [...] # Check Token via JWT validation
Expand Down
73 changes: 73 additions & 0 deletions lib/oidcc/plug/require_authorization.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
defmodule Oidcc.Plug.RequireAuthorization do
@moduledoc """
Ensure authorization token provided.

This module should be used together with `Oidcc.Plug.ExtractAuthorization`.

```elixir
defmodule SampleAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :sample_app

# ...

plug Oidcc.Plug.ExtractAuthorization

plug Oidcc.Plug.RequireAuthorization

# Check Token with `Oidcc.Plug.IntrospectToken`, `Oidcc.Plug.LoadUserinfo` or `Oidcc.Plug.ValidateJwtToken`

plug SampleAppWeb.Router
end
```
"""
@moduledoc since: "0.1.0"

@behaviour Plug

import Plug.Conn, only: [halt: 1, send_resp: 3, put_resp_header: 3]

alias Oidcc.Plug.ExtractAuthorization

@typedoc """
Plug Configuration Options

## Options

* `send_missing_token_response` - Customize Error Response for missing token
"""
@typedoc since: "0.1.0"
@type opts :: [
send_missing_token_response: (conn :: Plug.Conn.t() -> Plug.Conn.t())
]

@impl Plug
def init(opts),
do:
Keyword.validate!(opts,
send_missing_token_response: &__MODULE__.send_missing_token_response/1
)

@impl Plug
def call(%Plug.Conn{private: %{ExtractAuthorization => nil}} = conn, opts) do
send_missing_token_response = Keyword.fetch!(opts, :send_missing_token_response)

send_missing_token_response.(conn)
end

def call(%Plug.Conn{private: %{ExtractAuthorization => _access_token}} = conn, _opts), do: conn

def call(%Plug.Conn{} = _conn, _opts) do
raise """
The plug Oidcc.Plug.ExtractAuthorization must be run before this plug
"""
end

@doc false
@spec send_missing_token_response(conn :: Plug.Conn.t()) :: Plug.Conn.t()
def send_missing_token_response(conn) do
conn
|> halt()
|> put_resp_header("www-authenticate", "Bearer")
|> send_resp(:unauthorized, "The authorization token is required")
end
end
51 changes: 51 additions & 0 deletions test/oidcc/plug/require_authorization.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
defmodule Oidcc.Plug.RequireAuthorizationTest do
use ExUnit.Case, async: false
use Plug.Test

alias Oidcc.Plug.ExtractAuthorization
alias Oidcc.Plug.RequireAuthorization

doctest RequireAuthorization

describe inspect(&RequireAuthorization.call/2) do
test "errors without ExtractAuthorization" do
opts =
RequireAuthorization.init([])

assert_raise RuntimeError, fn ->
"get"
|> conn("/", "")
|> RequireAuthorization.call(opts)
end
end

test "send error response if no token provided" do
opts =
RequireAuthorization.init([])

assert %{
halted: true,
status: 401,
resp_headers: [
{"cache-control", "max-age=0, private, must-revalidate"},
{"www-authenticate", "Bearer"}
]
} =
"get"
|> conn("/", "")
|> put_private(ExtractAuthorization, nil)
|> RequireAuthorization.call(opts)
end

test "pass if token provided" do
opts =
RequireAuthorization.init([])

assert %{halted: false} =
"get"
|> conn("/", "")
|> put_private(ExtractAuthorization, "some_access_token")
|> RequireAuthorization.call(opts)
end
end
end
Loading