From d092ef3ea43355d5de93625c50afbc98b112f81f Mon Sep 17 00:00:00 2001 From: Paul Swartz Date: Mon, 4 Dec 2023 07:13:47 -0500 Subject: [PATCH] refactor: use `UeberauthOidcc` as a library --- lib/ueberauth/strategy/google.ex | 187 +++++++-------- lib/ueberauth/strategy/google/oauth.ex | 106 --------- lib/ueberauth_google/application.ex | 19 ++ mix.exs | 9 +- mix.lock | 12 +- test/strategy/google/oauth_test.exs | 20 -- test/strategy/google_test.exs | 303 +++++++++++++++++++++---- 7 files changed, 381 insertions(+), 275 deletions(-) delete mode 100644 lib/ueberauth/strategy/google/oauth.ex create mode 100644 lib/ueberauth_google/application.ex delete mode 100644 test/strategy/google/oauth_test.exs diff --git a/lib/ueberauth/strategy/google.ex b/lib/ueberauth/strategy/google.ex index e78e0db..6982db7 100644 --- a/lib/ueberauth/strategy/google.ex +++ b/lib/ueberauth/strategy/google.ex @@ -6,21 +6,31 @@ defmodule Ueberauth.Strategy.Google do use Ueberauth.Strategy, uid_field: :sub, default_scope: "email", - hd: nil, - userinfo_endpoint: "https://www.googleapis.com/oauth2/v3/userinfo" + hd: nil - alias Ueberauth.Auth.Info - alias Ueberauth.Auth.Credentials alias Ueberauth.Auth.Extra + @session_key "ueberauth_strategy_google" + @doc """ Handles initial request for Google authentication. """ def handle_request!(conn) do - scopes = conn.params["scope"] || option(conn, :default_scope) - - params = - [scope: scopes] + scopes = + String.split( + conn.params["scope"] || option(conn, :default_scope), + " " + ) + + scopes = + if "openid" in scopes do + scopes + else + ["openid"] ++ scopes + end + + authorization_params = + [] |> with_optional(:hd, conn) |> with_optional(:prompt, conn) |> with_optional(:access_type, conn) @@ -30,33 +40,39 @@ defmodule Ueberauth.Strategy.Google do |> with_param(:prompt, conn) |> with_param(:login_hint, conn) |> with_param(:hl, conn) - |> with_state_param(conn) - opts = oauth_client_options_from_conn(conn) - redirect!(conn, Ueberauth.Strategy.Google.OAuth.authorize_url!(params, opts)) + opts = + conn + |> options_from_conn() + |> Map.put(:scopes, scopes) + |> Map.put(:authorization_params, Map.new(authorization_params)) + + case UeberauthOidcc.Request.handle_request(opts, conn) do + {:ok, conn} -> + conn + + {:error, conn, reason} -> + UeberauthOidcc.Error.set_described_error(conn, reason, "error") + end end @doc """ Handles the callback from Google. """ - def handle_callback!(%Plug.Conn{params: %{"code" => code}} = conn) do - params = [code: code] - opts = oauth_client_options_from_conn(conn) + def handle_callback!(%Plug.Conn{} = conn) do + opts = options_from_conn(conn) - case Ueberauth.Strategy.Google.OAuth.get_access_token(params, opts) do - {:ok, token} -> - fetch_user(conn, token) + case UeberauthOidcc.Callback.handle_callback(opts, conn) do + {:ok, conn, token, userinfo} -> + conn + |> put_private(:google_token, token) + |> put_private(:google_user, userinfo) - {:error, {error_code, error_description}} -> - set_errors!(conn, [error(error_code, error_description)]) + {:error, conn, reason} -> + UeberauthOidcc.Error.set_described_error(conn, reason, "error") end end - @doc false - def handle_callback!(conn) do - set_errors!(conn, [error("missing_code", "No code received")]) - end - @doc false def handle_cleanup!(conn) do conn @@ -81,36 +97,23 @@ defmodule Ueberauth.Strategy.Google do """ def credentials(conn) do token = conn.private.google_token - scope_string = token.other_params["scope"] || "" - scopes = String.split(scope_string, " ") - - %Credentials{ - expires: !!token.expires_at, - expires_at: token.expires_at, - scopes: scopes, - token_type: Map.get(token, :token_type), - refresh_token: token.refresh_token, - token: token.access_token - } + credentials = UeberauthOidcc.Auth.credentials(token) + %{credentials | other: %{}} end @doc """ Fetches the fields to populate the info section of the `Ueberauth.Auth` struct. """ def info(conn) do + token = conn.private.google_token user = conn.private.google_user - %Info{ - email: user["email"], - first_name: user["given_name"], - image: user["picture"], - last_name: user["family_name"], - name: user["name"], - birthday: user["birthday"], - urls: %{ - profile: user["profile"], - website: user["hd"] - } + info = UeberauthOidcc.Auth.info(token, user) + + %{ + info + | birthday: info.birthday || user["birthday"], + urls: Map.put_new(info.urls, :website, user["hd"]) } end @@ -118,50 +121,25 @@ defmodule Ueberauth.Strategy.Google do Stores the raw information (including the token) obtained from the google callback. """ def extra(conn) do + creds = credentials(conn) + + # create a struct with the same format as the old token, even if we don't depend on OAuth2 + google_token = %{ + __struct__: OAuth2.AccessToken, + access_token: creds.token, + refresh_token: creds.refresh_token, + expires_at: creds.expires_at, + token_type: "Bearer" + } + %Extra{ raw_info: %{ - token: conn.private.google_token, + token: google_token, user: conn.private.google_user } } end - defp fetch_user(conn, token) do - conn = put_private(conn, :google_token, token) - - # userinfo_endpoint from https://accounts.google.com/.well-known/openid-configuration - # the userinfo_endpoint may be overridden in options when necessary. - resp = Ueberauth.Strategy.Google.OAuth.get(token, get_userinfo_endpoint(conn)) - - case resp do - {:ok, %OAuth2.Response{status_code: 401, body: _body}} -> - set_errors!(conn, [error("token", "unauthorized")]) - - {:ok, %OAuth2.Response{status_code: status_code, body: user}} - when status_code in 200..399 -> - put_private(conn, :google_user, user) - - {:error, %OAuth2.Response{status_code: status_code}} -> - set_errors!(conn, [error("OAuth2", status_code)]) - - {:error, %OAuth2.Error{reason: reason}} -> - set_errors!(conn, [error("OAuth2", reason)]) - end - end - - defp get_userinfo_endpoint(conn) do - case option(conn, :userinfo_endpoint) do - {:system, varname, default} -> - System.get_env(varname) || default - - {:system, varname} -> - System.get_env(varname) || Keyword.get(default_options(), :userinfo_endpoint) - - other -> - other - end - end - defp with_param(opts, key, conn) do if value = conn.params[to_string(key)], do: Keyword.put(opts, key, value), else: opts end @@ -170,18 +148,45 @@ defmodule Ueberauth.Strategy.Google do if option(conn, key), do: Keyword.put(opts, key, option(conn, key)), else: opts end - defp oauth_client_options_from_conn(conn) do - base_options = [redirect_uri: callback_url(conn)] - request_options = conn.private[:ueberauth_request_options].options + defp options_from_conn(conn) do + base_options = [ + issuer: UeberauthGoogle.ProviderConfiguration, + userinfo: true, + session_key: @session_key + ] - case {request_options[:client_id], request_options[:client_secret]} do - {nil, _} -> base_options - {_, nil} -> base_options - {id, secret} -> [client_id: id, client_secret: secret] ++ base_options - end + request_options = conn.private[:ueberauth_request_options].options + oauth_options = Application.get_env(:ueberauth, Ueberauth.Strategy.Google.OAuth) || [] + + [ + base_options, + request_options, + oauth_options + ] + |> UeberauthOidcc.Config.merge_and_expand_configuration() + |> generate_client_secret() + |> fix_token_url() end defp option(conn, key) do Keyword.get(options(conn), key, Keyword.get(default_options(), key)) end + + defp generate_client_secret(%{client_secret: {mod, fun}} = opts) do + Map.put(opts, :client_secret, apply(mod, fun, [Keyword.new(opts)])) + end + + defp generate_client_secret(opts) do + opts + end + + defp fix_token_url(%{token_url: token_endpoint} = opts) do + opts + |> Map.put(:token_endpoint, token_endpoint) + |> Map.delete(:token_url) + end + + defp fix_token_url(opts) do + opts + end end diff --git a/lib/ueberauth/strategy/google/oauth.ex b/lib/ueberauth/strategy/google/oauth.ex deleted file mode 100644 index 1f299bd..0000000 --- a/lib/ueberauth/strategy/google/oauth.ex +++ /dev/null @@ -1,106 +0,0 @@ -defmodule Ueberauth.Strategy.Google.OAuth do - @moduledoc """ - OAuth2 for Google. - - Add `client_id` and `client_secret` to your configuration: - - config :ueberauth, Ueberauth.Strategy.Google.OAuth, - client_id: System.get_env("GOOGLE_APP_ID"), - client_secret: System.get_env("GOOGLE_APP_SECRET") - - """ - use OAuth2.Strategy - - @defaults [ - strategy: __MODULE__, - site: "https://accounts.google.com", - authorize_url: "/o/oauth2/v2/auth", - token_url: "https://www.googleapis.com/oauth2/v4/token" - ] - - @doc """ - Construct a client for requests to Google. - - This will be setup automatically for you in `Ueberauth.Strategy.Google`. - - These options are only useful for usage outside the normal callback phase of Ueberauth. - """ - def client(opts \\ []) do - config = Application.get_env(:ueberauth, __MODULE__, []) - json_library = Ueberauth.json_library() - - @defaults - |> Keyword.merge(config) - |> Keyword.merge(opts) - |> resolve_values() - |> generate_secret() - |> OAuth2.Client.new() - |> OAuth2.Client.put_serializer("application/json", json_library) - end - - @doc """ - Provides the authorize url for the request phase of Ueberauth. No need to call this usually. - """ - def authorize_url!(params \\ [], opts \\ []) do - opts - |> client - |> OAuth2.Client.authorize_url!(params) - end - - def get(token, url, headers \\ [], opts \\ []) do - [token: token] - |> client - |> put_param("client_secret", client().client_secret) - |> OAuth2.Client.get(url, headers, opts) - end - - def get_access_token(params \\ [], opts \\ []) do - case opts |> client |> OAuth2.Client.get_token(params) do - {:error, %OAuth2.Response{body: %{"error" => error}} = response} -> - description = Map.get(response.body, "error_description", "") - {:error, {error, description}} - - {:error, %OAuth2.Error{reason: reason}} -> - {:error, {"error", to_string(reason)}} - - {:ok, %OAuth2.Client{token: %{access_token: nil} = token}} -> - %{"error" => error, "error_description" => description} = token.other_params - {:error, {error, description}} - - {:ok, %OAuth2.Client{token: token}} -> - {:ok, token} - end - end - - # Strategy Callbacks - - def authorize_url(client, params) do - OAuth2.Strategy.AuthCode.authorize_url(client, params) - end - - def get_token(client, params, headers) do - client - |> put_param("client_secret", client.client_secret) - |> put_header("Accept", "application/json") - |> OAuth2.Strategy.AuthCode.get_token(params, headers) - end - - defp resolve_values(list) do - for {key, value} <- list do - {key, resolve_value(value)} - end - end - - defp resolve_value({m, f, a}) when is_atom(m) and is_atom(f), do: apply(m, f, a) - defp resolve_value(v), do: v - - defp generate_secret(opts) do - if is_tuple(opts[:client_secret]) do - {module, fun} = opts[:client_secret] - secret = apply(module, fun, [opts]) - Keyword.put(opts, :client_secret, secret) - else - opts - end - end -end diff --git a/lib/ueberauth_google/application.ex b/lib/ueberauth_google/application.ex new file mode 100644 index 0000000..624f852 --- /dev/null +++ b/lib/ueberauth_google/application.ex @@ -0,0 +1,19 @@ +defmodule UeberauthGoogle.Application do + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [ + {Oidcc.ProviderConfiguration.Worker, + %{ + name: UeberauthGoogle.ProviderConfiguration, + issuer: "https://accounts.google.com" + }} + ] + + opts = [strategy: :one_for_one, name: UeberauthGoogle.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/mix.exs b/mix.exs index eff61f0..4adb337 100644 --- a/mix.exs +++ b/mix.exs @@ -9,7 +9,7 @@ defmodule UeberauthGoogle.Mixfile do app: :ueberauth_google, version: @version, name: "Üeberauth Google", - elixir: "~> 1.8", + elixir: ">= 1.14.4 and < 2.0.0", start_permanent: Mix.env() == :prod, package: package(), deps: deps(), @@ -19,14 +19,15 @@ defmodule UeberauthGoogle.Mixfile do def application do [ - extra_applications: [:logger, :oauth2, :ueberauth] + extra_applications: [:logger], + mod: {UeberauthGoogle.Application, []} ] end defp deps do [ - {:oauth2, "~> 1.0 or ~> 2.0"}, - {:ueberauth, "~> 0.10.0"}, + {:ueberauth_oidcc, "~> 0.3"}, + {:ueberauth, "~> 0.10.1"}, {:credo, ">= 0.0.0", only: [:dev, :test], runtime: false}, {:ex_doc, ">= 0.0.0", only: [:dev], runtime: false}, {:mock, "~> 0.3", only: :test} diff --git a/mix.lock b/mix.lock index 0c8eaaa..99ac691 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,5 @@ %{ "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, - "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"}, "earmark": {:hex, :earmark, "1.3.5", "0db71c8290b5bc81cb0101a2a507a76dca659513984d683119ee722828b424f6", [:mix], [], "hexpm", "762b999fd414fb41e297944228aa1de2cd4a3876a07f968c8b11d1e9a2190d07"}, "earmark_parser": {:hex, :earmark_parser, "1.4.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"}, @@ -9,9 +8,8 @@ "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, "exvcr": {:hex, :exvcr, "0.11.1", "a5e5f57a67538e032e16cfea6cfb1232314fb146e3ceedf1cde4a11f12fb7a58", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "984a4d52d9e01d5f0e28d45718565a41dffab3ac18e029ae45d42f16a2a58a1d"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, @@ -22,12 +20,12 @@ "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mock": {:hex, :mock, "0.3.7", "75b3bbf1466d7e486ea2052a73c6e062c6256fb429d6797999ab02fa32f29e03", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "4da49a4609e41fd99b7836945c26f373623ea968cfb6282742bcb94440cf7e5c"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, - "oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "oidcc": {:hex, :oidcc, "3.1.0", "0d290c56e050ad7f94e043d3f71de86b7d81e6ff84bdc536b8e061c8f71b739b", [:mix, :rebar3], [{:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.3.1", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "a4a3f527d90158d61f741ac4f16381fbc5dc83897e0e8cc0cdc1f1d3d6ced0bb"}, "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"}, "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry_registry": {:hex, :telemetry_registry, "0.3.1", "14a3319a7d9027bdbff7ebcacf1a438f5f5c903057b93aee484cca26f05bdcba", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6d0ca77b691cf854ed074b459a93b87f4c7f5512f8f7743c635ca83da81f939e"}, "ueberauth": {:hex, :ueberauth, "0.10.1", "6706b410ee6bd9d67eac983ed9dc7fdc1f06b18677d7b8ba71d5725e07cc8826", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bb715b562395c4cc26b2d8e637c6bb0eb8c67d50c0ea543c0f78f06b7e8efdb1"}, + "ueberauth_oidcc": {:hex, :ueberauth_oidcc, "0.3.0", "afda071b54fec405e4f45246d6aad962ac95e680c1f9552f92abbd9f4c409e03", [:mix], [{:oidcc, "~> 3.1.0", [hex: :oidcc, repo: "hexpm", optional: false]}, {:plug, "~> 1.11", [hex: :plug, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.10", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "e54713cbd7cfab834e94f51da5ad616aab4b181a6f5f4df772ce334431efedba"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/test/strategy/google/oauth_test.exs b/test/strategy/google/oauth_test.exs deleted file mode 100644 index 1039d76..0000000 --- a/test/strategy/google/oauth_test.exs +++ /dev/null @@ -1,20 +0,0 @@ -defmodule Ueberauth.Strategy.Google.OAuthTest do - use ExUnit.Case, async: true - - alias Ueberauth.Strategy.Google.OAuth - - defmodule MyApp.Google do - def client_secret(_opts), do: "custom_client_secret" - end - - describe "client/1" do - test "uses client secret in the config when it is not a tuple" do - assert %OAuth2.Client{client_secret: "client_secret"} = OAuth.client() - end - - test "generates client secret when it is using a tuple config" do - options = [client_secret: {MyApp.Google, :client_secret}] - assert %OAuth2.Client{client_secret: "custom_client_secret"} = OAuth.client(options) - end - end -end diff --git a/test/strategy/google_test.exs b/test/strategy/google_test.exs index 234a880..ce7ac80 100644 --- a/test/strategy/google_test.exs +++ b/test/strategy/google_test.exs @@ -4,18 +4,21 @@ defmodule Ueberauth.Strategy.GoogleTest do import Mock import Plug.Conn + alias Plug.Conn.Query import Ueberauth.Strategy.Helpers setup_with_mocks([ - {OAuth2.Client, [:passthrough], + {UeberauthOidcc.Callback, [:passthrough], [ - get_token: &oauth2_get_token/2, - get: &oauth2_get/4 + handle_callback: &oidcc_handle_callback/2 ]} ]) do # Create a connection with Ueberauth's CSRF cookies so they can be recycled during tests routes = Ueberauth.init([]) - csrf_conn = conn(:get, "/auth/google", %{}) |> Ueberauth.call(routes) + + csrf_conn = + conn(:get, "/auth/google", %{}) |> init_test_session(%{}) |> Ueberauth.call(routes) + csrf_state = with_state_param([], csrf_conn) |> Keyword.get(:state) {:ok, csrf_conn: csrf_conn, csrf_state: csrf_state} @@ -31,34 +34,161 @@ defmodule Ueberauth.Strategy.GoogleTest do end end - defp token(client, opts), do: {:ok, %{client | token: OAuth2.AccessToken.new(opts)}} - defp response(body, code \\ 200), do: {:ok, %OAuth2.Response{status_code: code, body: body}} + def oidcc_handle_callback(opts, conn) + + def oidcc_handle_callback(_opts, %{params: %{"code" => "success_code"}} = conn) do + token = %Oidcc.Token{ + access: %Oidcc.Token.Access{ + token: "success_token" + }, + id: %Oidcc.Token.Id{ + claims: %{} + } + } + + userinfo = %{ + "sub" => "1234_fred", + "name" => "Fred Jones", + "email" => "fred_jones@example.com" + } + + {:ok, conn, token, userinfo} + end + + def oidcc_handle_callback(_opts, %{params: %{"code" => "uid_code"}} = conn) do + token = %Oidcc.Token{ + access: %Oidcc.Token.Access{ + token: "uid_token" + }, + id: %Oidcc.Token.Id{ + claims: %{} + } + } + + userinfo = %{ + "uid_field" => "1234_daphne", + "name" => "Daphne Blake" + } + + {:ok, conn, token, userinfo} + end - def oauth2_get_token(client, code: "success_code"), do: token(client, "success_token") - def oauth2_get_token(client, code: "uid_code"), do: token(client, "uid_token") - def oauth2_get_token(client, code: "userinfo_code"), do: token(client, "userinfo_token") - def oauth2_get_token(_client, code: "oauth2_error"), do: {:error, %OAuth2.Error{reason: :timeout}} + def oidcc_handle_callback( + %{userinfo_endpoint: "example.com/shaggy"}, + %{params: %{"code" => "userinfo_code"}} = conn + ) do + token = %Oidcc.Token{ + access: %Oidcc.Token.Access{ + token: "userinfo_token" + }, + id: %Oidcc.Token.Id{ + claims: %{} + } + } + + userinfo = %{"sub" => "1234_shaggy", "name" => "Norville Rogers"} + + {:ok, conn, token, userinfo} + end - def oauth2_get_token(_client, code: "error_response"), - do: {:error, %OAuth2.Response{body: %{"error" => "some error", "error_description" => "something went wrong"}}} + def oidcc_handle_callback( + %{userinfo_endpoint: "example.com/scooby"}, + %{params: %{"code" => "userinfo_code"}} = conn + ) do + token = %Oidcc.Token{ + access: %Oidcc.Token.Access{ + token: "userinfo_token" + }, + id: %Oidcc.Token.Id{ + claims: %{} + } + } + + userinfo = %{"sub" => "1234_scooby", "name" => "Scooby Doo"} + + {:ok, conn, token, userinfo} + end + + def oidcc_handle_callback( + _opts, + %{params: %{"code" => "userinfo_code"}} = conn + ) do + token = %Oidcc.Token{ + access: %Oidcc.Token.Access{ + token: "userinfo_token" + }, + id: %Oidcc.Token.Id{ + claims: %{} + } + } + + userinfo = %{ + "sub" => "1234_velma", + "name" => "Velma Dinkley" + } + + {:ok, conn, token, userinfo} + end + + def oidcc_handle_callback( + %{client_secret: "custom_client_secret"}, + %{params: %{"code" => "client_secret_code"}} = conn + ) do + token = %Oidcc.Token{ + access: %Oidcc.Token.Access{ + token: "success_token" + }, + id: %Oidcc.Token.Id{ + claims: %{} + } + } + + userinfo = %{ + "sub" => "1234_fred", + "name" => "Fred Jones", + "email" => "fred_jones@example.com" + } + + {:ok, conn, token, userinfo} + end - def oauth2_get_token(_client, code: "error_response_no_description"), - do: {:error, %OAuth2.Response{body: %{"error" => "internal_failure"}}} + def oidcc_handle_callback( + _opts, + %{params: %{"code" => "oauth2_error"}} = conn + ) do + {:error, conn, :timeout} + end - def oauth2_get(%{token: %{access_token: "success_token"}}, _url, _, _), - do: response(%{"sub" => "1234_fred", "name" => "Fred Jones", "email" => "fred_jones@example.com"}) + def oidcc_handle_callback( + _opts, + %{params: %{"code" => "error_response"}} = conn + ) do + {:error, conn, + {:http_error, 401, %{"error" => "some error", "error_description" => "something went wrong"}}} + end - def oauth2_get(%{token: %{access_token: "uid_token"}}, _url, _, _), - do: response(%{"uid_field" => "1234_daphne", "name" => "Daphne Blake"}) + def oidcc_handle_callback( + _opts, + %{params: %{"code" => "error_response_no_description"}} = conn + ) do + {:error, conn, {:http_error, 401, %{"error" => "internal_failure"}}} + end - def oauth2_get(%{token: %{access_token: "userinfo_token"}}, "https://www.googleapis.com/oauth2/v3/userinfo", _, _), - do: response(%{"sub" => "1234_velma", "name" => "Velma Dinkley"}) + def oidcc_handle_callback(_opts, conn) do + {:error, conn, :not_defined} + end - def oauth2_get(%{token: %{access_token: "userinfo_token"}}, "example.com/shaggy", _, _), - do: response(%{"sub" => "1234_shaggy", "name" => "Norville Rogers"}) + def oidcc_retrieve_userinfo( + "userinfo_token", + %{provider_configuration: %{userinfo_endpoint: "example.com/scooby"}}, + _opts + ) do + {:ok, %{"sub" => "1234_scooby", "name" => "Scooby Doo"}} + end - def oauth2_get(%{token: %{access_token: "userinfo_token"}}, "example.com/scooby", _, _), - do: response(%{"sub" => "1234_scooby", "name" => "Scooby Doo"}) + def oidcc_retrieve_userinfo(_token, _client_context, _opts) do + {:error, :not_defined} + end defp set_csrf_cookies(conn, csrf_conn) do conn @@ -68,9 +198,11 @@ defmodule Ueberauth.Strategy.GoogleTest do end test "handle_request! redirects to appropriate auth uri" do - conn = conn(:get, "/auth/google", %{hl: "es"}) + conn = conn(:get, "/auth/google", %{hl: "es"}) |> init_test_session(%{}) + # Make sure the hd and scope params are included for good measure - routes = Ueberauth.init() |> set_options(conn, hd: "example.com", default_scope: "email openid") + routes = + Ueberauth.init() |> set_options(conn, hd: "example.com", default_scope: "email openid") resp = Ueberauth.call(conn, routes) @@ -88,12 +220,16 @@ defmodule Ueberauth.Strategy.GoogleTest do "scope" => "email openid", "hd" => "example.com", "hl" => "es" - } = Plug.Conn.Query.decode(redirect_uri.query) + } = Query.decode(redirect_uri.query) end - test "handle_callback! assigns required fields on successful auth", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do + test "handle_callback! assigns required fields on successful auth", %{ + csrf_state: csrf_state, + csrf_conn: csrf_conn + } do conn = - conn(:get, "/auth/google/callback", %{code: "success_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + conn(:get, "/auth/google/callback", %{code: "success_code", state: csrf_state}) + |> set_csrf_cookies(csrf_conn) routes = Ueberauth.init([]) assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) @@ -103,35 +239,56 @@ defmodule Ueberauth.Strategy.GoogleTest do assert auth.uid == "1234_fred" end - test "uid_field is picked according to the specified option", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do - conn = conn(:get, "/auth/google/callback", %{code: "uid_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + test "uid_field is picked according to the specified option", %{ + csrf_state: csrf_state, + csrf_conn: csrf_conn + } do + conn = + conn(:get, "/auth/google/callback", %{code: "uid_code", state: csrf_state}) + |> set_csrf_cookies(csrf_conn) + routes = Ueberauth.init() |> set_options(conn, uid_field: "uid_field") assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) assert auth.info.name == "Daphne Blake" assert auth.uid == "1234_daphne" end - test "userinfo is fetched according to userinfo_endpoint", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do + test "userinfo is fetched according to userinfo_endpoint", %{ + csrf_state: csrf_state, + csrf_conn: csrf_conn + } do conn = - conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) + |> set_csrf_cookies(csrf_conn) routes = Ueberauth.init() |> set_options(conn, userinfo_endpoint: "example.com/shaggy") assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) assert auth.info.name == "Norville Rogers" end - test "userinfo can be set via runtime config with default", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do + test "userinfo can be set via runtime config with default", %{ + csrf_state: csrf_state, + csrf_conn: csrf_conn + } do conn = - conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) + |> set_csrf_cookies(csrf_conn) + + routes = + Ueberauth.init() + |> set_options(conn, userinfo_endpoint: {:system, "NOT_SET", "example.com/shaggy"}) - routes = Ueberauth.init() |> set_options(conn, userinfo_endpoint: {:system, "NOT_SET", "example.com/shaggy"}) assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) assert auth.info.name == "Norville Rogers" end - test "userinfo uses default library value if runtime env not found", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do + test "userinfo uses default library value if runtime env not found", %{ + csrf_state: csrf_state, + csrf_conn: csrf_conn + } do conn = - conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) + |> set_csrf_cookies(csrf_conn) routes = Ueberauth.init() |> set_options(conn, userinfo_endpoint: {:system, "NOT_SET"}) assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) @@ -140,17 +297,46 @@ defmodule Ueberauth.Strategy.GoogleTest do test "userinfo can be set via runtime config", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do conn = - conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) + |> set_csrf_cookies(csrf_conn) + + routes = + Ueberauth.init() |> set_options(conn, userinfo_endpoint: {:system, "UEBERAUTH_SCOOBY_DOO"}) - routes = Ueberauth.init() |> set_options(conn, userinfo_endpoint: {:system, "UEBERAUTH_SCOOBY_DOO"}) System.put_env("UEBERAUTH_SCOOBY_DOO", "example.com/scooby") assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) assert auth.info.name == "Scooby Doo" System.delete_env("UEBERAUTH_SCOOBY_DOO") end + test "client_secret can be set via {mod, fun} tuple (taking the opts)", %{ + csrf_state: csrf_state, + csrf_conn: csrf_conn + } do + conn = + conn(:get, "/auth/google/callback", %{code: "client_secret_code", state: csrf_state}) + |> set_csrf_cookies(csrf_conn) + + routes = Ueberauth.init() |> set_options(conn, custom_config: :value) + + env_config = Application.get_env(:ueberauth, Ueberauth.Strategy.Google.OAuth) + + Application.put_env( + :ueberauth, + Ueberauth.Strategy.Google.OAuth, + Keyword.put(env_config, :client_secret, {__MODULE__.ClientSecret, :client_secret}) + ) + + on_exit(fn -> + Application.put_env(:ueberauth, Ueberauth.Strategy.Google.OAuth, env_config) + end) + + assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) + assert auth.info.name == "Fred Jones" + end + test "state param is present in the redirect uri" do - conn = conn(:get, "/auth/google", %{}) + conn = conn(:get, "/auth/google", %{}) |> init_test_session(%{}) routes = Ueberauth.init() resp = Ueberauth.call(conn, routes) @@ -165,22 +351,35 @@ defmodule Ueberauth.Strategy.GoogleTest do describe "error handling" do test "handle_callback! handles Oauth2.Error", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do conn = - conn(:get, "/auth/google/callback", %{code: "oauth2_error", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + conn(:get, "/auth/google/callback", %{code: "oauth2_error", state: csrf_state}) + |> set_csrf_cookies(csrf_conn) routes = Ueberauth.init([]) assert %Plug.Conn{assigns: %{ueberauth_failure: failure}} = Ueberauth.call(conn, routes) - assert %Ueberauth.Failure{errors: [%Ueberauth.Failure.Error{message: "timeout", message_key: "error"}]} = failure + + assert %Ueberauth.Failure{ + errors: [%Ueberauth.Failure.Error{message: ":timeout", message_key: "error"}] + } = failure end - test "handle_callback! handles error response", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do + test "handle_callback! handles error response", %{ + csrf_state: csrf_state, + csrf_conn: csrf_conn + } do conn = - conn(:get, "/auth/google/callback", %{code: "error_response", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + conn(:get, "/auth/google/callback", %{code: "error_response", state: csrf_state}) + |> set_csrf_cookies(csrf_conn) routes = Ueberauth.init([]) assert %Plug.Conn{assigns: %{ueberauth_failure: failure}} = Ueberauth.call(conn, routes) assert %Ueberauth.Failure{ - errors: [%Ueberauth.Failure.Error{message: "something went wrong", message_key: "some error"}] + errors: [ + %Ueberauth.Failure.Error{ + message: "something went wrong", + message_key: "some error" + } + ] } = failure end @@ -189,7 +388,10 @@ defmodule Ueberauth.Strategy.GoogleTest do csrf_conn: csrf_conn } do conn = - conn(:get, "/auth/google/callback", %{code: "error_response_no_description", state: csrf_state}) + conn(:get, "/auth/google/callback", %{ + code: "error_response_no_description", + state: csrf_state + }) |> set_csrf_cookies(csrf_conn) routes = Ueberauth.init([]) @@ -200,4 +402,11 @@ defmodule Ueberauth.Strategy.GoogleTest do } = failure end end + + defmodule ClientSecret do + def client_secret(opts) do + assert Keyword.get(opts, :custom_config) == :value + "custom_client_secret" + end + end end