Skip to content

Commit

Permalink
Add require authorization plug (#21)
Browse files Browse the repository at this point in the history
* Add require authorization plug

* Remove duplicate codes from RequireAuthorization plug

* Update README

* Test added for require authorization plug

---------

Co-authored-by: Ali Shirvani <[email protected]>
  • Loading branch information
alishir and Ali Shirvani authored Jun 26, 2024
1 parent c5609a6 commit 35ce3ad
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 0 deletions.
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

0 comments on commit 35ce3ad

Please sign in to comment.