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

[MAJOR] Implement Device Flow Strategy #94

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ erl_crash.dump
/tmp
.DS_Store
/.elixir_ls
*.swp
/plts
.tool-versions
18 changes: 13 additions & 5 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
use Mix.Config

config :ex_oauth2_provider, namespace: Dummy

config :ex_oauth2_provider, ExOauth2Provider,
repo: Dummy.Repo,
resource_owner: Dummy.Users.User,
default_scopes: ~w(public),
device_flow_verification_uri: "https://test.site.net/device",
grant_flows: ~w(
authorization_code
client_credentials
device_code
),
optional_scopes: ~w(read write),
password_auth: {Dummy.Auth, :auth},
use_refresh_token: true,
repo: Dummy.Repo,
resource_owner: Dummy.Users.User,
revoke_refresh_token_on_use: true,
grant_flows: ~w(authorization_code client_credentials)
use_refresh_token: true

config :ex_oauth2_provider, Dummy.Repo,
database: "ex_oauth2_provider_test",
pool: Ecto.Adapters.SQL.Sandbox,
priv: "test/support/priv"
priv: "test/support/priv",
username: "postgres",
password: "postgres"
18 changes: 12 additions & 6 deletions lib/ex_oauth2_provider.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ defmodule ExOauth2Provider do
expire.
"""

@behaviour ExOauth2Provider.Behaviours.TokenAuthentication

alias ExOauth2Provider.{Config, AccessTokens}

@doc """
Expand All @@ -48,9 +50,10 @@ defmodule ExOauth2Provider do
{:ok, access_token}
{:error, reason}
"""
@spec authenticate_token(binary(), keyword()) :: {:ok, map()} | {:error, any()}
@impl ExOauth2Provider.Behaviours.TokenAuthentication
def authenticate_token(token, config \\ [])
def authenticate_token(nil, _config), do: {:error, :token_inaccessible}

def authenticate_token(token, config) do
token
|> load_access_token(config)
Expand All @@ -61,37 +64,40 @@ defmodule ExOauth2Provider do

defp load_access_token(token, config) do
case AccessTokens.get_by_token(token, config) do
nil -> {:error, :token_not_found}
nil -> {:error, :token_not_found}
access_token -> {:ok, access_token}
end
end

defp maybe_revoke_previous_refresh_token({:error, error}, _config), do: {:error, error}

defp maybe_revoke_previous_refresh_token({:ok, access_token}, config) do
case Config.refresh_token_revoked_on_use?(config) do
true -> revoke_previous_refresh_token(access_token, config)
true -> revoke_previous_refresh_token(access_token, config)
false -> {:ok, access_token}
end
end

defp revoke_previous_refresh_token(access_token, config) do
case AccessTokens.revoke_previous_refresh_token(access_token, config) do
{:error, _any} -> {:error, :no_association_found}
{:error, _any} -> {:error, :no_association_found}
{:ok, _access_token} -> {:ok, access_token}
end
end

defp validate_access_token({:error, error}), do: {:error, error}

defp validate_access_token({:ok, access_token}) do
case AccessTokens.is_accessible?(access_token) do
true -> {:ok, access_token}
true -> {:ok, access_token}
false -> {:error, :token_inaccessible}
end
end

defp load_resource_owner({:error, error}, _config), do: {:error, error}

defp load_resource_owner({:ok, access_token}, config) do
repo = Config.repo(config)
repo = Config.repo(config)
access_token = repo.preload(access_token, :resource_owner)

{:ok, access_token}
Expand Down
32 changes: 26 additions & 6 deletions lib/ex_oauth2_provider/access_grants/access_grants.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ defmodule ExOauth2Provider.AccessGrants do
"""
@spec get_active_grant_for(Application.t(), binary(), keyword()) :: AccessGrant.t() | nil
def get_active_grant_for(application, token, config \\ []) do
config
|> Config.access_grant()
|> Config.repo(config).get_by(application_id: application.id, token: token)
|> Expirable.filter_expired()
|> Revocable.filter_revoked()
get_active_grant_with_criteria(
[application_id: application.id, token: token],
config
)
end

@doc """
Expand All @@ -42,12 +41,33 @@ defmodule ExOauth2Provider.AccessGrants do
{:error, %Ecto.Changeset{}}

"""
@spec create_grant(Ecto.Schema.t(), Application.t(), map(), keyword()) :: {:ok, AccessGrant.t()} | {:error, term()}
@spec create_grant(Ecto.Schema.t(), Application.t(), map(), keyword()) ::
{:ok, AccessGrant.t()} | {:error, term()}
def create_grant(resource_owner, application, attrs, config \\ []) do
config
|> Config.access_grant()
|> struct(resource_owner: resource_owner, application: application)
|> AccessGrant.changeset(attrs, config)
|> Config.repo(config).insert()
end

@doc """
Retrieve active grant for the given resource owner and token.
"""
@spec get_active_grant_for_owner_by_token(Ecto.Schema.t(), binary(), keyword()) ::
AccessGrant.t() | nil
def get_active_grant_for_owner_by_token(owner, token, config \\ []) do
get_active_grant_with_criteria(
[token: token, resource_owner_id: owner.id],
config
)
end

defp get_active_grant_with_criteria(criteria, config) do
config
|> Config.access_grant()
|> Config.repo(config).get_by(criteria)
|> Expirable.filter_expired()
|> Revocable.filter_revoked()
end
end
27 changes: 15 additions & 12 deletions lib/ex_oauth2_provider/applications/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ defmodule ExOauth2Provider.Applications.Application do
@doc false
def attrs() do
[
{:name, :string, null: false},
{:uid, :string, null: false},
{:secret, :string, null: false, default: ""},
{:redirect_uri, :string, null: false},
{:scopes, :string, null: false, default: ""},
{:is_trusted, :boolean, default: false, null: false},
{:name, :string},
{:redirect_uri, :string},
{:scopes, :string, default: ""},
{:secret, :string, default: ""},
{:uid, :string}
]
end

Expand All @@ -67,7 +68,7 @@ defmodule ExOauth2Provider.Applications.Application do
use ExOauth2Provider.Schema, unquote(config)

# For Phoenix integrations
if Code.ensure_loaded?(Phoenix.Param), do: @derive {Phoenix.Param, key: :uid}
if Code.ensure_loaded?(Phoenix.Param), do: @derive({Phoenix.Param, key: :uid})

import unquote(__MODULE__), only: [application_fields: 0]
end
Expand All @@ -87,7 +88,7 @@ defmodule ExOauth2Provider.Applications.Application do
def changeset(application, params, config \\ []) do
application
|> maybe_new_application_changeset(params, config)
|> Changeset.cast(params, [:name, :secret, :redirect_uri, :scopes])
|> Changeset.cast(params, [:is_trusted, :name, :secret, :redirect_uri, :scopes])
|> Changeset.validate_required([:name, :uid, :redirect_uri])
|> validate_secret_not_nil()
|> Scopes.validate_scopes(nil, config)
Expand All @@ -98,13 +99,13 @@ defmodule ExOauth2Provider.Applications.Application do
defp validate_secret_not_nil(changeset) do
case Changeset.get_field(changeset, :secret) do
nil -> Changeset.add_error(changeset, :secret, "can't be blank")
_ -> changeset
_ -> changeset
end
end

defp maybe_new_application_changeset(application, params, config) do
case Ecto.get_meta(application, :state) do
:built -> new_application_changeset(application, params, config)
:built -> new_application_changeset(application, params, config)
:loaded -> application
end
end
Expand All @@ -127,18 +128,20 @@ defmodule ExOauth2Provider.Applications.Application do
url
|> RedirectURI.validate(config)
|> case do
{:error, error} -> Changeset.add_error(changeset, :redirect_uri, error)
{:ok, _} -> changeset
end
{:error, error} -> Changeset.add_error(changeset, :redirect_uri, error)
{:ok, _} -> changeset
end
end)
end

defp put_uid(%{changes: %{uid: _}} = changeset), do: changeset

defp put_uid(%{} = changeset) do
Changeset.change(changeset, %{uid: Utils.generate_token()})
end

defp put_secret(%{changes: %{secret: _}} = changeset), do: changeset

defp put_secret(%{} = changeset) do
Changeset.change(changeset, %{secret: Utils.generate_token()})
end
Expand Down
23 changes: 23 additions & 0 deletions lib/ex_oauth2_provider/behaviours/skip_authorization.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule ExOauth2Provider.Behaviours.SkipAuthorization do
@moduledoc """
Define the rules used to determine if authorization can be skipped. If your
app has unique criteria then implement it.

For example:

defmodule MyModule do
@behaviour ExOauth2Provider.Behaviors.SkipAuthorization

def skip_authorization(user, application) do
user.super_cool? || application.trusted?
end
end
"""
alias ExOauth2Provider.Applications.Application
alias ExOauth2Provider.Schema

@callback skip_authorization?(
user :: Schema.t(),
application :: Application.t()
) :: boolean()
end
8 changes: 8 additions & 0 deletions lib/ex_oauth2_provider/behaviours/token_authentication.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule ExOauth2Provider.Behaviours.TokenAuthentication do
@moduledoc """
Simple behavior for defining a custom token authentication strategy.
"""

@callback authenticate_token(token :: binary(), config :: keyword()) ::
{:ok, map()} | {:error, any()}
end
Loading