From edd21a45ce95ef9b7f47a4cac280fd013b532da8 Mon Sep 17 00:00:00 2001 From: Rostyslav Khoptiy Date: Mon, 14 Jun 2021 00:41:24 +0200 Subject: [PATCH 01/10] implement token introspection --- lib/ex_oauth2_provider/oauth2/token.ex | 8 ++- .../oauth2/token/strategy/introspect.ex | 61 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex diff --git a/lib/ex_oauth2_provider/oauth2/token.ex b/lib/ex_oauth2_provider/oauth2/token.ex index a47bce74..79b0081a 100644 --- a/lib/ex_oauth2_provider/oauth2/token.ex +++ b/lib/ex_oauth2_provider/oauth2/token.ex @@ -5,7 +5,8 @@ defmodule ExOauth2Provider.Token do alias ExOauth2Provider.{ Config, Token.Revoke, - Utils.Error} + Utils.Error, + Token.Introspect} alias Ecto.Schema @doc """ @@ -82,4 +83,9 @@ defmodule ExOauth2Provider.Token do """ @spec revoke(map(), keyword()) :: {:ok, Schema.t()} | {:error, map(), term()} def revoke(request, config \\ []), do: Revoke.revoke(request, config) + + @doc """ + Introspect an access or refresh token as per https://datatracker.ietf.org/doc/html/rfc7662 + """ + def introspect(params, config \\ []), do: Introspect.introspect(params, config) end diff --git a/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex b/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex new file mode 100644 index 00000000..23aaebd6 --- /dev/null +++ b/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex @@ -0,0 +1,61 @@ +defmodule ExOauth2Provider.Token.Introspect do + @moduledoc """ + Functions for dealing with token introspection. + """ + alias ExOauth2Provider.{ + AccessTokens, + Utils.Error, + Mixin.Expirable, + Mixin.Revocable, + Config + } + + # 'token_type_hint' query param is not needed to guess if the token is an access or refresh token and can be safely ignored: https://datatracker.ietf.org/doc/html/rfc7662#section-2.1 + def introspect(%{"token" => token}, config \\ []) do + {:ok, %{token: token}} + |> check_access_token(config) + |> check_refresh_token(config) + |> build_response(config) + end + + def introspect(_, _), do: Error.invalid_request() + + defp check_access_token({:ok, %{token: token} = params}, config) do + access_token = AccessTokens.get_by_token(token, config) + + if access_token == nil || Expirable.is_expired?(access_token) || + Revocable.is_revoked?(access_token) do + {:ok, Map.merge(params, %{active: false})} + else + {:ok, Map.merge(params, %{active: true, token: access_token})} + end + end + + defp check_refresh_token({:ok, %{active: false, token: token} = params}, config) do + refresh_token = AccessTokens.get_by_refresh_token(token, config) + + if refresh_token == nil || Revocable.is_revoked?(refresh_token) do + {:ok, Map.merge(params, %{active: false})} + else + {:ok, Map.merge(params, %{active: true, token: refresh_token})} + end + end + + defp check_refresh_token({arg, params}, _config), do: {arg, params} + + defp build_response({:ok, %{active: false}}, _), do: {:ok, %{active: false}} + + defp build_response({:ok, %{active: true, token: token}}, config) do + token = Config.repo(config).preload(token, :application) + + # as defined in https://datatracker.ietf.org/doc/html/rfc7662#section-2.2 + # TODO: implement 'exp' and 'iat' + {:ok, + %{ + active: true, + scope: token.scopes, + token_type: "bearer", + client_id: token.application.uid + }} + end +end From a7b032ea9fc0ee0e9e9f1ad435d4dcc4bcabad8d Mon Sep 17 00:00:00 2001 From: Rostyslav Khoptiy Date: Tue, 15 Jun 2021 20:53:25 +0200 Subject: [PATCH 02/10] add client authentication for token introspection, restrict to self-emitted tokens --- .../access_tokens/access_tokens.ex | 18 +++++++ .../oauth2/token/strategy/introspect.ex | 50 ++++++++++++------- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/lib/ex_oauth2_provider/access_tokens/access_tokens.ex b/lib/ex_oauth2_provider/access_tokens/access_tokens.ex index bc35d24e..088562b6 100644 --- a/lib/ex_oauth2_provider/access_tokens/access_tokens.ex +++ b/lib/ex_oauth2_provider/access_tokens/access_tokens.ex @@ -34,6 +34,24 @@ defmodule ExOauth2Provider.AccessTokens do |> Config.repo(config).get_by(token: token) end + @doc """ + Gets a single access token belonging to an application. + + ## Examples + + iex> get_by_token_for(application, "c341a5c7b331ef076eb4954668d54f590e0009e06b81b100191aa22c93044f3d", otp_app: :my_app) + %OauthAccessToken{} + + iex> get_by_token_for(application, "75d72f326a69444a9287ea264617058dbbfe754d7071b8eef8294cbf4e7e0fdc", otp_app: :my_app) + nil + """ + @spec get_by_token_for(Application.t(), binary(), keyword()) :: AccessToken.t() | nil + def get_by_token_for(application, token, config \\ []) do + config + |> Config.access_token() + |> Config.repo(config).get_by(application_id: application.id, token: token) + end + @doc """ Gets an access token by the refresh token. diff --git a/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex b/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex index 23aaebd6..359b6173 100644 --- a/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex +++ b/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex @@ -5,14 +5,17 @@ defmodule ExOauth2Provider.Token.Introspect do alias ExOauth2Provider.{ AccessTokens, Utils.Error, + Token.Utils, + Token.Utils.Response, Mixin.Expirable, Mixin.Revocable, Config } # 'token_type_hint' query param is not needed to guess if the token is an access or refresh token and can be safely ignored: https://datatracker.ietf.org/doc/html/rfc7662#section-2.1 - def introspect(%{"token" => token}, config \\ []) do - {:ok, %{token: token}} + def introspect(%{"token" => _} = request, config \\ []) do + {:ok, %{request: request}} + |> Utils.load_client(config) |> check_access_token(config) |> check_refresh_token(config) |> build_response(config) @@ -20,28 +23,37 @@ defmodule ExOauth2Provider.Token.Introspect do def introspect(_, _), do: Error.invalid_request() - defp check_access_token({:ok, %{token: token} = params}, config) do - access_token = AccessTokens.get_by_token(token, config) + defp check_access_token({:ok, %{client: client, request: %{"token" => token}} = params}, config) do + access_token = AccessTokens.get_by_token_for(client, token, config) - if access_token == nil || Expirable.is_expired?(access_token) || - Revocable.is_revoked?(access_token) do - {:ok, Map.merge(params, %{active: false})} - else - {:ok, Map.merge(params, %{active: true, token: access_token})} - end + params = + if access_token == nil || Expirable.is_expired?(access_token) || + Revocable.is_revoked?(access_token) do + Map.merge(params, %{active: false}) + else + Map.merge(params, %{active: true, token: access_token}) + end + + {:ok, params} end - defp check_refresh_token({:ok, %{active: false, token: token} = params}, config) do - refresh_token = AccessTokens.get_by_refresh_token(token, config) + defp check_access_token({:error, _} = req, _config), do: req + + defp check_refresh_token({:ok, %{client: client, active: false, request: %{"token" => token}} = params}, config) do + refresh_token = AccessTokens.get_by_refresh_token_for(client, token, config) - if refresh_token == nil || Revocable.is_revoked?(refresh_token) do - {:ok, Map.merge(params, %{active: false})} - else - {:ok, Map.merge(params, %{active: true, token: refresh_token})} - end + params = + if refresh_token == nil || Revocable.is_revoked?(refresh_token) do + Map.merge(params, %{active: false}) + else + Map.merge(params, %{active: true, token: refresh_token}) + end + + {:ok, params} end - defp check_refresh_token({arg, params}, _config), do: {arg, params} + defp check_refresh_token({:ok, %{active: true}} = req, _config), do: req + defp check_refresh_token({:error, _} = req, _config), do: req defp build_response({:ok, %{active: false}}, _), do: {:ok, %{active: false}} @@ -58,4 +70,6 @@ defmodule ExOauth2Provider.Token.Introspect do client_id: token.application.uid }} end + + defp build_response({:error, _} = params, config), do: Response.response(params, config) end From c3ad11e04a8c60b30ccd87a539748d6935a89c2d Mon Sep 17 00:00:00 2001 From: Rostyslav Khoptiy Date: Tue, 15 Jun 2021 21:41:09 +0200 Subject: [PATCH 03/10] add expires information to token introspection --- .../oauth2/token/strategy/introspect.ex | 25 +++++++++++++------ lib/ex_oauth2_provider/schema.ex | 11 ++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex b/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex index 359b6173..2c453df0 100644 --- a/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex +++ b/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex @@ -9,7 +9,8 @@ defmodule ExOauth2Provider.Token.Introspect do Token.Utils.Response, Mixin.Expirable, Mixin.Revocable, - Config + Config, + Schema } # 'token_type_hint' query param is not needed to guess if the token is an access or refresh token and can be safely ignored: https://datatracker.ietf.org/doc/html/rfc7662#section-2.1 @@ -31,7 +32,7 @@ defmodule ExOauth2Provider.Token.Introspect do Revocable.is_revoked?(access_token) do Map.merge(params, %{active: false}) else - Map.merge(params, %{active: true, token: access_token}) + Map.merge(params, %{active: true, token: access_token, type: :access_token}) end {:ok, params} @@ -46,7 +47,7 @@ defmodule ExOauth2Provider.Token.Introspect do if refresh_token == nil || Revocable.is_revoked?(refresh_token) do Map.merge(params, %{active: false}) else - Map.merge(params, %{active: true, token: refresh_token}) + Map.merge(params, %{active: true, token: refresh_token, type: :refresh_token}) end {:ok, params} @@ -55,21 +56,29 @@ defmodule ExOauth2Provider.Token.Introspect do defp check_refresh_token({:ok, %{active: true}} = req, _config), do: req defp check_refresh_token({:error, _} = req, _config), do: req - defp build_response({:ok, %{active: false}}, _), do: {:ok, %{active: false}} - - defp build_response({:ok, %{active: true, token: token}}, config) do + defp build_response({:ok, %{active: true, token: token, type: token_type}}, config) do token = Config.repo(config).preload(token, :application) + created_at = Schema.unix_time_for(token.inserted_at) + expires_at = + if token_type == :access_token do + created_at + token.expires_in + else # refresh tokens don't expire + nil + end + # as defined in https://datatracker.ietf.org/doc/html/rfc7662#section-2.2 - # TODO: implement 'exp' and 'iat' {:ok, %{ active: true, scope: token.scopes, token_type: "bearer", - client_id: token.application.uid + client_id: token.application.uid, + iat: created_at, + exp: expires_at }} end + defp build_response({:ok, %{active: false}}, _), do: {:ok, %{active: false}} defp build_response({:error, _} = params, config), do: Response.response(params, config) end diff --git a/lib/ex_oauth2_provider/schema.ex b/lib/ex_oauth2_provider/schema.ex index ad9419d1..10f33552 100644 --- a/lib/ex_oauth2_provider/schema.ex +++ b/lib/ex_oauth2_provider/schema.ex @@ -91,4 +91,15 @@ defmodule ExOauth2Provider.Schema do def __timestamp__(type) do type.from_unix!(System.system_time(:microsecond), :microsecond) end + + def unix_time_for(%DateTime{} = datetime) do + DateTime.to_unix(datetime) + end + def unix_time_for(%NaiveDateTime{} = naive) do + DateTime.from_naive!(naive, "Etc/UTC") + |> unix_time_for() + end + def unix_time_for(date) when is_struct(date) do + date.__struct__.to_unix(date) + end end From e9d9a0e0d8ffe9796ea7275914a015dd7dc8ef21 Mon Sep 17 00:00:00 2001 From: Rostyslav Khoptiy Date: Tue, 15 Jun 2021 21:51:41 +0200 Subject: [PATCH 04/10] add token introspection 'sub' field --- lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex b/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex index 2c453df0..f03bad00 100644 --- a/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex +++ b/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex @@ -74,8 +74,9 @@ defmodule ExOauth2Provider.Token.Introspect do scope: token.scopes, token_type: "bearer", client_id: token.application.uid, + exp: expires_at, iat: created_at, - exp: expires_at + sub: token.resource_owner_id }} end From c5752b33ae4510366a8322b65d2e9cbdb5a59dfa Mon Sep 17 00:00:00 2001 From: Rostyslav Khoptiy Date: Sat, 21 Aug 2021 17:49:35 +0200 Subject: [PATCH 05/10] add introspection tests --- .../access_tokens/access_tokens_test.exs | 14 ++ .../token/strategy/introspection_test.exs | 179 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 test/ex_oauth2_provider/oauth2/token/strategy/introspection_test.exs diff --git a/test/ex_oauth2_provider/access_tokens/access_tokens_test.exs b/test/ex_oauth2_provider/access_tokens/access_tokens_test.exs index 6ebe0c07..ac4acef2 100644 --- a/test/ex_oauth2_provider/access_tokens/access_tokens_test.exs +++ b/test/ex_oauth2_provider/access_tokens/access_tokens_test.exs @@ -17,6 +17,20 @@ defmodule ExOauth2Provider.AccessTokensTest do assert id == access_token.id end + test "get_by_token_for/2", %{user: user, application: application} do + {:ok, access_token} = AccessTokens.create_token(user, %{application: application}, otp_app: :ex_oauth2_provider) + + assert %OauthAccessToken{id: id} = AccessTokens.get_by_token_for(application, access_token.token, otp_app: :ex_oauth2_provider) + assert id == access_token.id + end + + test "get_by_token_for/2 different application", %{user: user, application: application} do + {:ok, access_token} = AccessTokens.create_token(user, %{application: application}, otp_app: :ex_oauth2_provider) + + other_application = Fixtures.application(resource_owner: user, uid: "other",) + assert AccessTokens.get_by_token_for(other_application, access_token.token, otp_app: :ex_oauth2_provider) == nil + end + test "get_by_refresh_token/2", %{user: user} do assert {:ok, access_token} = AccessTokens.create_token(user, %{use_refresh_token: true}, otp_app: :ex_oauth2_provider) diff --git a/test/ex_oauth2_provider/oauth2/token/strategy/introspection_test.exs b/test/ex_oauth2_provider/oauth2/token/strategy/introspection_test.exs new file mode 100644 index 00000000..b96ae8b8 --- /dev/null +++ b/test/ex_oauth2_provider/oauth2/token/strategy/introspection_test.exs @@ -0,0 +1,179 @@ +defmodule ExOauth2Provider.Token.Strategy.IntrospectionTest do + use ExOauth2Provider.TestCase + + alias ExOauth2Provider.{AccessTokens, Token, Schema} + alias ExOauth2Provider.Test.{Fixtures, QueryHelpers} + + @client_id "Jf5rM8hQBc" + @client_secret "secret" + @invalid_client_error %{ + error: :invalid_client, + error_description: + "Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method." + } + + setup do + user = Fixtures.resource_owner() + + application = + Fixtures.application( + resource_owner: user, + uid: @client_id, + secret: @client_secret, + scopes: "app:read app:write" + ) + + access_token = + Fixtures.access_token( + resource_owner: user, + application: application, + use_refresh_token: true, + scopes: "app:read" + ) + + expired_token = + Fixtures.access_token( + resource_owner: user, + application: application, + use_refresh_token: true, + scopes: "app:read", + expires_in: -1000 + ) + + revoked_token = + Fixtures.access_token( + resource_owner: user, + application: application, + use_refresh_token: true, + scopes: "app:read" + ) + revoked_token = AccessTokens.revoke!(revoked_token, otp_app: :ex_oauth2_provider) + + valid_request = %{ + "client_id" => @client_id, + "client_secret" => @client_secret, + "token" => access_token.token + } + + {:ok, + %{ + access_token: access_token, + expired_token: expired_token, + revoked_token: revoked_token, + valid_request: valid_request, + user: user + }} + end + + test "#introspect/2 error when invalid client", %{valid_request: valid_request} do + params = Map.merge(valid_request, %{"client_id" => "invalid"}) + + assert Token.introspect(params, otp_app: :ex_oauth2_provider) == + {:error, @invalid_client_error, :unprocessable_entity} + end + + test "#introspect/2 error when invalid secret", %{valid_request: valid_request} do + params = Map.merge(valid_request, %{"client_secret" => "invalid"}) + + assert Token.introspect(params, otp_app: :ex_oauth2_provider) == + {:error, @invalid_client_error, :unprocessable_entity} + end + + test "#introspect/2 non-existing token", %{valid_request: valid_request} do + params = Map.merge(valid_request, %{"token" => "invalid"}) + + assert Token.introspect(params, otp_app: :ex_oauth2_provider) == {:ok, %{active: false}} + end + + test "#introspect/2 access token", %{ + valid_request: valid_request, + access_token: access_token, + user: user + } do + created_at = Schema.unix_time_for(access_token.inserted_at) + expected_introspection = + {:ok, + %{ + active: true, + client_id: @client_id, + exp: created_at + access_token.expires_in, + iat: created_at, + scope: access_token.scopes, + sub: user.id, + token_type: "bearer" + }} + + assert Token.introspect(valid_request, otp_app: :ex_oauth2_provider) == expected_introspection + end + + test "#introspect/2 access token owned by another application", %{ + valid_request: valid_request, + access_token: access_token + } do + new_application = Fixtures.application(uid: "new_app", client_secret: "new") + QueryHelpers.change!(access_token, application_id: new_application.id) + + assert Token.introspect(valid_request, otp_app: :ex_oauth2_provider) == {:ok, %{active: false}} + end + + test "#introspect/2 refresh token", %{ + valid_request: valid_request, + access_token: access_token + } do + params = Map.merge(valid_request, %{"token" => access_token.refresh_token}) + + created_at = Schema.unix_time_for(access_token.inserted_at) + expected_introspection = + {:ok, + %{ + active: true, + client_id: @client_id, + exp: nil, + iat: created_at, + scope: access_token.scopes, + sub: user.id, + token_type: "bearer" + }} + + assert Token.introspect(params, otp_app: :ex_oauth2_provider) == expected_introspection + end + + test "#introspect/2 expired access token", %{ + valid_request: valid_request, + expired_token: expired_token + } do + params = Map.merge(valid_request, %{"token" => expired_token.token}) + + assert Token.introspect(params, otp_app: :ex_oauth2_provider) == {:ok, %{active: false}} + end + + test "#introspect/2 expired refresh token", %{ + valid_request: valid_request, + expired_token: expired_token + } do + params = Map.merge(valid_request, %{"token" => expired_token.refresh_token}) + {status, introspection} = Token.introspect(params, otp_app: :ex_oauth2_provider) + + assert status == :ok + assert introspection.active + end + + test "#introspect/2 revoked access token", %{ + valid_request: valid_request, + revoked_token: revoked_token, + user: user + } do + params = Map.merge(valid_request, %{"token" => revoked_token.token}) + + assert Token.introspect(params, otp_app: :ex_oauth2_provider) == {:ok, %{active: false}} + end + + test "#introspect/2 revoked refresh token", %{ + valid_request: valid_request, + revoked_token: revoked_token + } do + params = Map.merge(valid_request, %{"token" => revoked_token.refresh_token}) + + assert Token.introspect(params, otp_app: :ex_oauth2_provider) == {:ok, %{active: false}} + end +end From e2dfb15418e92393b98165cb9777b0b32eaf54a6 Mon Sep 17 00:00:00 2001 From: Rostyslav Khoptiy Date: Sat, 21 Aug 2021 18:06:25 +0200 Subject: [PATCH 06/10] fix linter warning --- lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex b/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex index f03bad00..111d43a6 100644 --- a/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex +++ b/lib/ex_oauth2_provider/oauth2/token/strategy/introspect.ex @@ -13,8 +13,10 @@ defmodule ExOauth2Provider.Token.Introspect do Schema } + def introspect(params, config \\ []) + # 'token_type_hint' query param is not needed to guess if the token is an access or refresh token and can be safely ignored: https://datatracker.ietf.org/doc/html/rfc7662#section-2.1 - def introspect(%{"token" => _} = request, config \\ []) do + def introspect(%{"token" => _} = request, config) do {:ok, %{request: request}} |> Utils.load_client(config) |> check_access_token(config) From d141b2636113ef7e7ba3690971165b5f811ebfb1 Mon Sep 17 00:00:00 2001 From: Rostyslav Khoptiy Date: Sat, 21 Aug 2021 18:06:30 +0200 Subject: [PATCH 07/10] update readme --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 83b491c4..0a7bea2d 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,33 @@ end Revocation will return `{:ok, %{}}` status even if the token is invalid. +### Token introspection + +Check access token or refresh token for validity and meta-data. [See RFC-7662](https://datatracker.ietf.org/doc/html/rfc7662) + +```elixir +# GET /oauth/introspect?client_id=CLIENT_ID&client_secret=CLIENT_SECRET&token=ACCESS_TOKEN +# or +# GET /oauth/introspect?client_id=CLIENT_ID&client_secret=CLIENT_SECRET&token=REFRESH_TOKEN +case ExOauth2Provider.Token.introspect(params, otp_app: :my_app) do + {:ok, introspection} -> # JSON response + {:error, error, http_status} -> # JSON response +end +``` + +Example `introspection` value: +```elixir +%{ + active: true, + client_id: "0f3e0eee9e70c6aa833bc03ba7e635e1842e92a82e14d7d2222221111", + exp: 1629563742, # not present for refresh tokens + iat: 1629556542, + scope: "read write", + sub: 1, + token_type: "bearer" +} +``` + ### Authorization code flow in a Single Page Application ExOauth2Provider doesn't support **implicit** grant flow. Instead you should set up an application with no client secret, and use the **Authorize code** grant flow. `client_secret` isn't required unless it has been set for the application. From 5a89026adc2098caf2a41e00470132266486a75c Mon Sep 17 00:00:00 2001 From: Rostyslav Khoptiy Date: Sat, 21 Aug 2021 18:31:25 +0200 Subject: [PATCH 08/10] update introspection tests --- .../token/strategy/introspection_test.exs | 146 +++++++++++------- 1 file changed, 90 insertions(+), 56 deletions(-) diff --git a/test/ex_oauth2_provider/oauth2/token/strategy/introspection_test.exs b/test/ex_oauth2_provider/oauth2/token/strategy/introspection_test.exs index b96ae8b8..fe7356a2 100644 --- a/test/ex_oauth2_provider/oauth2/token/strategy/introspection_test.exs +++ b/test/ex_oauth2_provider/oauth2/token/strategy/introspection_test.exs @@ -31,24 +31,6 @@ defmodule ExOauth2Provider.Token.Strategy.IntrospectionTest do scopes: "app:read" ) - expired_token = - Fixtures.access_token( - resource_owner: user, - application: application, - use_refresh_token: true, - scopes: "app:read", - expires_in: -1000 - ) - - revoked_token = - Fixtures.access_token( - resource_owner: user, - application: application, - use_refresh_token: true, - scopes: "app:read" - ) - revoked_token = AccessTokens.revoke!(revoked_token, otp_app: :ex_oauth2_provider) - valid_request = %{ "client_id" => @client_id, "client_secret" => @client_secret, @@ -57,11 +39,10 @@ defmodule ExOauth2Provider.Token.Strategy.IntrospectionTest do {:ok, %{ + user: user + application: application, access_token: access_token, - expired_token: expired_token, - revoked_token: revoked_token, valid_request: valid_request, - user: user }} end @@ -91,6 +72,7 @@ defmodule ExOauth2Provider.Token.Strategy.IntrospectionTest do user: user } do created_at = Schema.unix_time_for(access_token.inserted_at) + expected_introspection = {:ok, %{ @@ -113,16 +95,19 @@ defmodule ExOauth2Provider.Token.Strategy.IntrospectionTest do new_application = Fixtures.application(uid: "new_app", client_secret: "new") QueryHelpers.change!(access_token, application_id: new_application.id) - assert Token.introspect(valid_request, otp_app: :ex_oauth2_provider) == {:ok, %{active: false}} + assert Token.introspect(valid_request, otp_app: :ex_oauth2_provider) == + {:ok, %{active: false}} end test "#introspect/2 refresh token", %{ valid_request: valid_request, - access_token: access_token + access_token: access_token, + user: user } do params = Map.merge(valid_request, %{"token" => access_token.refresh_token}) created_at = Schema.unix_time_for(access_token.inserted_at) + expected_introspection = {:ok, %{ @@ -138,42 +123,91 @@ defmodule ExOauth2Provider.Token.Strategy.IntrospectionTest do assert Token.introspect(params, otp_app: :ex_oauth2_provider) == expected_introspection end - test "#introspect/2 expired access token", %{ - valid_request: valid_request, - expired_token: expired_token - } do - params = Map.merge(valid_request, %{"token" => expired_token.token}) - - assert Token.introspect(params, otp_app: :ex_oauth2_provider) == {:ok, %{active: false}} - end - - test "#introspect/2 expired refresh token", %{ - valid_request: valid_request, - expired_token: expired_token - } do - params = Map.merge(valid_request, %{"token" => expired_token.refresh_token}) - {status, introspection} = Token.introspect(params, otp_app: :ex_oauth2_provider) - - assert status == :ok - assert introspection.active - end - - test "#introspect/2 revoked access token", %{ - valid_request: valid_request, - revoked_token: revoked_token, - user: user - } do - params = Map.merge(valid_request, %{"token" => revoked_token.token}) + describe "with expired token" do + setup %{ + application: application, + access_token: access_token, + valid_request: valid_request, + user: user + } do + expired_token = + Fixtures.access_token( + resource_owner: user, + application: application, + use_refresh_token: true, + scopes: "app:read", + expires_in: -1000 + ) - assert Token.introspect(params, otp_app: :ex_oauth2_provider) == {:ok, %{active: false}} + {:ok, + %{ + valid_request: valid_request, + expired_token: expired_token + }} + end + + test "#introspect/2 expired access token", %{ + valid_request: valid_request, + expired_token: expired_token + } do + params = Map.merge(valid_request, %{"token" => expired_token.token}) + + assert Token.introspect(params, otp_app: :ex_oauth2_provider) == {:ok, %{active: false}} + end + + test "#introspect/2 expired refresh token", %{ + valid_request: valid_request, + expired_token: expired_token + } do + params = Map.merge(valid_request, %{"token" => expired_token.refresh_token}) + {status, introspection} = Token.introspect(params, otp_app: :ex_oauth2_provider) + + assert status == :ok + assert introspection.active + end end - test "#introspect/2 revoked refresh token", %{ - valid_request: valid_request, - revoked_token: revoked_token - } do - params = Map.merge(valid_request, %{"token" => revoked_token.refresh_token}) + describe "with revoked token" do + setup %{ + application: application, + access_token: access_token, + valid_request: valid_request, + user: user + } do + revoked_token = + Fixtures.access_token( + resource_owner: user, + application: application, + use_refresh_token: true, + scopes: "app:read" + ) + + revoked_token = AccessTokens.revoke!(revoked_token, otp_app: :ex_oauth2_provider) - assert Token.introspect(params, otp_app: :ex_oauth2_provider) == {:ok, %{active: false}} + {:ok, + %{ + valid_request: valid_request, + revoked_token: revoked_token + }} + end + + test "#introspect/2 revoked access token", %{ + valid_request: valid_request, + revoked_token: revoked_token, + user: user + } do + params = Map.merge(valid_request, %{"token" => revoked_token.token}) + + assert Token.introspect(params, otp_app: :ex_oauth2_provider) == {:ok, %{active: false}} + end + + test "#introspect/2 revoked refresh token", %{ + valid_request: valid_request, + revoked_token: revoked_token + } do + params = Map.merge(valid_request, %{"token" => revoked_token.refresh_token}) + + assert Token.introspect(params, otp_app: :ex_oauth2_provider) == {:ok, %{active: false}} + end end end From ba7644478e328e00869edfbd1417801a45588b33 Mon Sep 17 00:00:00 2001 From: Rostyslav Khoptiy Date: Sat, 21 Aug 2021 18:31:31 +0200 Subject: [PATCH 09/10] fix whitespace --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0a7bea2d..c11f9480 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Check access token or refresh token for validity and meta-data. [See RFC-7662](h # or # GET /oauth/introspect?client_id=CLIENT_ID&client_secret=CLIENT_SECRET&token=REFRESH_TOKEN case ExOauth2Provider.Token.introspect(params, otp_app: :my_app) do - {:ok, introspection} -> # JSON response + {:ok, introspection} -> # JSON response {:error, error, http_status} -> # JSON response end ``` From da8e5dffadb8b77888272a14a26b4f16a4b67f03 Mon Sep 17 00:00:00 2001 From: Rostyslav Khoptiy Date: Sat, 21 Aug 2021 18:57:12 +0200 Subject: [PATCH 10/10] update token introspection tests --- .../token/strategy/introspection_test.exs | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/test/ex_oauth2_provider/oauth2/token/strategy/introspection_test.exs b/test/ex_oauth2_provider/oauth2/token/strategy/introspection_test.exs index fe7356a2..cab604b3 100644 --- a/test/ex_oauth2_provider/oauth2/token/strategy/introspection_test.exs +++ b/test/ex_oauth2_provider/oauth2/token/strategy/introspection_test.exs @@ -39,10 +39,10 @@ defmodule ExOauth2Provider.Token.Strategy.IntrospectionTest do {:ok, %{ - user: user + user: user, application: application, access_token: access_token, - valid_request: valid_request, + valid_request: valid_request }} end @@ -123,6 +123,34 @@ defmodule ExOauth2Provider.Token.Strategy.IntrospectionTest do assert Token.introspect(params, otp_app: :ex_oauth2_provider) == expected_introspection end + describe "access token with refresh token disabled" do + setup %{ + application: application, + valid_request: valid_request, + user: user + } do + access_token = + Fixtures.access_token( + resource_owner: user, + application: application, + use_refresh_token: false, + scopes: "app:read" + ) + + valid_request = Map.merge(valid_request, %{"token" => access_token.token}) + + {:ok, + %{ + valid_request: valid_request, + access_token: access_token + }} + end + + test "#introspect/2", %{ valid_request: valid_request } do + assert {:ok, %{active: true}} = Token.introspect(valid_request, otp_app: :ex_oauth2_provider) + end + end + describe "with expired token" do setup %{ application: application, @@ -160,10 +188,8 @@ defmodule ExOauth2Provider.Token.Strategy.IntrospectionTest do expired_token: expired_token } do params = Map.merge(valid_request, %{"token" => expired_token.refresh_token}) - {status, introspection} = Token.introspect(params, otp_app: :ex_oauth2_provider) - assert status == :ok - assert introspection.active + assert {:ok, %{active: true}} = Token.introspect(params, otp_app: :ex_oauth2_provider) end end