Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ erl_crash.dump
/tmp
.DS_Store
/.elixir_ls
.tool-versions
/priv/pg_data
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,21 @@ You'll need to create the migration file and schema modules with the argument `-
mix ex_oauth2_provider.install --binary-id
```

## Development

Starting postgrest for test
( one will need to modify the pg_hba.conf after booting postges for the first time)
( notes are in the script below )
```bash
./postgres.zsh
```

Testing binary_id
```bash
mix clean && UUID=1 mix test
```


## Acknowledgement

This library was made thanks to [doorkeeper](https://github.com/doorkeeper-gem/doorkeeper), [guardian](https://github.com/ueberauth/guardian) and [authable](https://github.com/mustafaturan/authable), that gave the conceptual building blocks.
Expand Down
5 changes: 5 additions & 0 deletions lib/ex_oauth2_provider/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ defmodule ExOauth2Provider.Config do
def use_refresh_token?(config),
do: get(config, :use_refresh_token, false)

# Require PKCE for code grant (disabled by default) recommend enabling
@spec use_pkce?(keyword()) :: boolean()
def use_pkce?(config),
do: get(config, :use_pkce, false)

# Password auth method to use. Disabled by default. When set, it'll enable
# password auth strategy. Set config as:
# `password_auth: {MyModule, :my_auth_method}`
Expand Down
10 changes: 10 additions & 0 deletions lib/ex_oauth2_provider/oauth2/authorization/strategy/code.ex
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,22 @@ defmodule ExOauth2Provider.Authorization.Code do

defp validate_request({:error, params}, _config), do: {:error, params}
defp validate_request({:ok, params}, config) do
# TODO smells like railway pattern here - meta opportunity
{:ok, params}
|> validate_pkce_params(Config.use_pkce?(config))
|> validate_resource_owner()
|> validate_redirect_uri(config)
|> validate_scopes(config)
end


defp validate_pkce_params({:error, params} , _), do: {:error, params}
defp validate_pkce_params({:ok, params}, false), do: {:ok, params}
defp validate_pkce_params({:ok, %{request: %{"code_challenge" => _code_challenge, "code_challenge_method" => _code_challenge_method}}} = input, true), do: input
defp validate_pkce_params({:ok, params}, true), do: Error.add_error({:ok, params}, Error.invalid_pkce_auth())


defp validate_resource_owner({:error, params}), do: {:error, params}
defp validate_resource_owner({:ok, %{resource_owner: resource_owner} = params}) do
case resource_owner do
%{__struct__: _} -> {:ok, params}
Expand Down
8 changes: 8 additions & 0 deletions lib/ex_oauth2_provider/oauth2/utils/error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ defmodule ExOauth2Provider.Utils.Error do
{:error, %{error: :invalid_request, error_description: msg}, :bad_request}
end

@doc false
@spec invalid_pkce_auth() :: {:error, map(), atom()}
def invalid_pkce_auth do
msg = "PKCE enabled: The request is missing required parameters"
{:error, %{error: :invalid_pkce, error_description: msg}, :bad_request}
end


@doc false
@spec invalid_client() :: {:error, map(), atom()}
def invalid_client do
Expand Down
10 changes: 10 additions & 0 deletions postgres.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env zsh
SCRIPT_DIR=$0:a:h
PG_NAME=postgres-oauth2-provider
echo " NOTE: For first time use i manually hack \"host all all all trust\" into priv/pg_data/pg_hba.conf"
echo " after the director has been initialized"
echo " then I stop this script and restart it "
docker stop $PG_NAME
docker run --rm --name $PG_NAME -e POSTGRES_USER=$USER -e POSTGRES_PASSWORD=secret -p 5432:5432 -v $SCRIPT_DIR/priv/pg_data:/var/lib/postgresql/data postgres


4 changes: 2 additions & 2 deletions test/ex_oauth2_provider/config_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ defmodule ExOauth2Provider.ConfigTest do
assert Config.repo(otp_app: :my_app) == Dummy.Repo

Application.delete_env(:ex_oauth2_provider, ExOauth2Provider)
Application.put_env(:my_app, ExOauth2Provider, repo: Dummy.Repo)
Application.put_env(:my_app, ExOauth2Provider, repo: :different_repo)

assert Config.repo(otp_app: :my_app) == Dummy.Repo
assert Config.repo(otp_app: :my_app) == :different_repo

Application.delete_env(:my_app, ExOauth2Provider)

Expand Down
114 changes: 94 additions & 20 deletions test/ex_oauth2_provider/oauth2/authorization/strategy/code_test.exs
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
defmodule ExOauth2Provider.Authorization.CodeTest do
use ExOauth2Provider.TestCase

alias ExOauth2Provider.{Authorization, Config, Scopes}
alias ExOauth2Provider.{Authorization, Config, Scopes, Utils}
alias ExOauth2Provider.Test.{Fixtures, QueryHelpers}
alias Dummy.{OauthAccessGrants.OauthAccessGrant, Repo}

@client_id "Jf5rM8hQBc"
@valid_request %{"client_id" => @client_id, "response_type" => "code", "scope" => "app:read app:write"}
@invalid_request %{error: :invalid_request,
error_description: "The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."
}
@invalid_client %{error: :invalid_client,
error_description: "Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method."
}
@invalid_scope %{error: :invalid_scope,
error_description: "The requested scope is invalid, unknown, or malformed."
}
@invalid_redirect_uri %{error: :invalid_redirect_uri,
error_description: "The redirect uri included is not valid."
}
@access_denied %{error: :access_denied,
error_description: "The resource owner or authorization server denied the request."
}
@code_challenge_method "S256"
@custom_native_redirect "app://callback"
@client_id "Jf5rM8hQBc"
@valid_request %{"client_id" => @client_id, "response_type" => "code"}
@invalid_request %{error: :invalid_request,
error_description: "The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."
}
@invalid_client %{error: :invalid_client,
error_description: "Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method."
}
@invalid_scope %{error: :invalid_scope,
error_description: "The requested scope is invalid, unknown, or malformed."
}
@invalid_redirect_uri %{error: :invalid_redirect_uri,
error_description: "The redirect uri included is not valid."
}
@access_denied %{error: :access_denied,
error_description: "The resource owner or authorization server denied the request."
}
@pkce_enabled_auth_missing_fields elem(Utils.Error.invalid_pkce_auth(), 1)

setup do
resource_owner = Fixtures.resource_owner()
Expand Down Expand Up @@ -83,6 +86,12 @@ defmodule ExOauth2Provider.Authorization.CodeTest do
%{resource_owner: resource_owner, application: application}
end

test "with no scope", %{resource_owner: resource_owner, application: application} do
request = Map.delete(@valid_request, "scope")

assert Authorization.preauthorize(resource_owner, request, otp_app: :ex_oauth2_provider) == {:ok, application, []}
end

test "with limited server scope", %{resource_owner: resource_owner, application: application} do
request = Map.merge(@valid_request, %{"scope" => "read"})

Expand Down Expand Up @@ -127,19 +136,84 @@ defmodule ExOauth2Provider.Authorization.CodeTest do
assert Authorization.authorize(resource_owner, request, otp_app: :ex_oauth2_provider) == {:error, @invalid_scope, :unprocessable_entity}
end

describe "#authorize/3 custom native_redirect" do
setup %{resource_owner: resource_owner, application: application} do
config = Application.get_env(:ex_oauth2_provider, ExOauth2Provider)
config = [{:native_redirect_uri, @custom_native_redirect} | config]
config = Application.put_env(:ex_oauth2_provider, ExOauth2Provider, config)

application = QueryHelpers.change!(application, scopes: "")
application = QueryHelpers.change!(application, redirect_uri: @custom_native_redirect)

verifier = :crypto.strong_rand_bytes(128)
#code_verifier= Base.url_encode64(verifier, padding: false)

%{resource_owner: resource_owner, application: application, verifier: verifier}
end
test "generates a grant", %{verifier: verifier, resource_owner: resource_owner} do

assert {:native_redirect, %{code: code}} = Authorization.authorize(resource_owner, @valid_request, otp_app: :ex_oauth2_provider)
access_grant = Repo.get_by(OauthAccessGrant, token: code)
assert access_grant.resource_owner_id == resource_owner.id
assert access_grant.redirect_uri == "app://callback"
end
end

describe "#authorize/3 with PKCE enabled" do
setup %{resource_owner: resource_owner, application: application} do
config = Application.get_env(:ex_oauth2_provider, ExOauth2Provider)
config = [{:use_pkce, true} | config]
config = Application.put_env(:ex_oauth2_provider, ExOauth2Provider, config)

application = QueryHelpers.change!(application, scopes: "")

verifier = :crypto.strong_rand_bytes(128)
#code_verifier= Base.url_encode64(verifier, padding: false)

%{resource_owner: resource_owner, application: application, verifier: verifier}
end

test "returns an error without code_challenge", %{verifier: verifier, resource_owner: resource_owner} do
# a valid request without a code_challenge is invalid with pkce on
missing_code_challenge_request = Map.delete(@valid_request, "code_challenge")
assert Authorization.authorize(resource_owner, missing_code_challenge_request, otp_app: :ex_oauth2_provider) == {:error, @pkce_enabled_auth_missing_fields, :bad_request}
end

test "generates a grant with a code_verifier", %{verifier: verifier, resource_owner: resource_owner} do
code_challenge = Base.url_encode64(:crypto.hash(:sha256, verifier))
request = Map.merge(@valid_request, %{"code_challenge" => code_challenge, "code_challenge_method" => @code_challenge_method })

assert {:native_redirect, %{code: code}} = Authorization.authorize(resource_owner, request, otp_app: :ex_oauth2_provider)
access_grant = Repo.get_by(OauthAccessGrant, token: code)
assert access_grant.resource_owner_id == resource_owner.id
# todo
# assert access_grant.code_challege_method == @code_challenge_method
# assert access_grant.code_challege == code_chalenge_method
end
end


describe "#authorize/3 when application has no scope" do
setup %{resource_owner: resource_owner, application: application} do
application = QueryHelpers.change!(application, scopes: "")

%{resource_owner: resource_owner, application: application}
end

test "generates grant with no scope passed", %{resource_owner: resource_owner} do
request = Map.delete(@valid_request, "scope")
assert {:native_redirect, %{code: code}} = Authorization.authorize(resource_owner, request, otp_app: :ex_oauth2_provider)

access_grant = Repo.get_by(OauthAccessGrant, token: code)
assert access_grant.resource_owner_id == resource_owner.id
end

test "error when invalid server scope", %{resource_owner: resource_owner} do
request = Map.merge(@valid_request, %{"scope" => "public profile"})
assert Authorization.authorize(resource_owner, request, otp_app: :ex_oauth2_provider) == {:error, @invalid_scope, :unprocessable_entity}
end

test "generates grant", %{resource_owner: resource_owner} do
test "generates grant with public scope", %{resource_owner: resource_owner} do
request = Map.merge(@valid_request, %{"scope" => "public"})
assert {:native_redirect, %{code: code}} = Authorization.authorize(resource_owner, request, otp_app: :ex_oauth2_provider)

Expand All @@ -160,7 +234,7 @@ defmodule ExOauth2Provider.Authorization.CodeTest do

assert access_grant.resource_owner_id == resource_owner.id
assert access_grant.expires_in == Config.authorization_code_expires_in(otp_app: :ex_oauth2_provider)
assert access_grant.scopes == @valid_request["scope"]
assert access_grant.scopes == ""
end

test "#authorize/3 generates grant with redirect uri", %{resource_owner: resource_owner, application: application} do
Expand Down