diff --git a/lib/assent/strategies/oauth.ex b/lib/assent/strategies/oauth.ex index 34a40f1..3d1668b 100644 --- a/lib/assent/strategies/oauth.ex +++ b/lib/assent/strategies/oauth.ex @@ -57,6 +57,13 @@ defmodule Assent.Strategy.OAuth do UnexpectedResponseError } + @type session_params :: %{ + oauth_token_secret: binary() + } + + @type on_authorize_url :: {:ok, %{session_params: session_params(), url: binary()}} | {:error, term()} + @type on_callback :: {:ok, %{user: map(), token: map()}} | {:error, term()} + @doc """ Generate authorization URL for request phase. @@ -71,9 +78,7 @@ defmodule Assent.Strategy.OAuth do - `:authorization_params` - The authorization parameters, defaults to `[]` """ @impl true - @spec authorize_url(Config.t()) :: - {:ok, %{url: binary(), session_params: %{oauth_token_secret: binary()}}} - | {:error, term()} + @spec authorize_url(Config.t()) :: on_authorize_url() def authorize_url(config) do case Config.fetch(config, :redirect_uri) do {:ok, redirect_uri} -> authorize_url(config, redirect_uri) @@ -342,8 +347,7 @@ defmodule Assent.Strategy.OAuth do `authorize_url/1`, optional """ @impl true - @spec callback(Config.t(), map(), atom()) :: - {:ok, %{user: map(), token: map()}} | {:error, term()} + @spec callback(Config.t(), map(), atom()) :: on_callback() def callback(config, params, strategy \\ __MODULE__) do with {:ok, oauth_token} <- fetch_oauth_token(params), {:ok, oauth_verifier} <- fetch_oauth_verifier(params), diff --git a/lib/assent/strategies/oauth/base.ex b/lib/assent/strategies/oauth/base.ex index e4ae8e6..13c2f62 100644 --- a/lib/assent/strategies/oauth/base.ex +++ b/lib/assent/strategies/oauth/base.ex @@ -65,15 +65,14 @@ defmodule Assent.Strategy.OAuth.Base do end end - @spec authorize_url(Keyword.t(), module()) :: {:ok, %{url: binary()}} | {:error, term()} + @spec authorize_url(Keyword.t(), module()) :: OAuth.on_authorize_url() def authorize_url(config, strategy) do config |> set_config(strategy) |> OAuth.authorize_url() end - @spec callback(Keyword.t(), map(), module()) :: - {:ok, %{user: map(), token: map()}} | {:error, term()} + @spec callback(Keyword.t(), map(), module()) :: OAuth.on_callback() def callback(config, params, strategy) do config = set_config(config, strategy) diff --git a/lib/assent/strategies/oauth2.ex b/lib/assent/strategies/oauth2.ex index 53f5c38..0172bad 100644 --- a/lib/assent/strategies/oauth2.ex +++ b/lib/assent/strategies/oauth2.ex @@ -96,6 +96,9 @@ defmodule Assent.Strategy.OAuth2 do optional(:code_challenge_method) => binary() } + @type on_authorize_url :: {:ok, %{session_params: session_params(), url: binary()}} | {:error, term()} + @type on_callback :: {:ok, %{user: map(), token: map()}} | {:error, term()} + @doc """ Generate authorization URL for request phase. @@ -108,8 +111,7 @@ defmodule Assent.Strategy.OAuth2 do - `:authorization_params` - The authorization parameters, defaults to `[]` """ @impl true - @spec authorize_url(Config.t()) :: - {:ok, %{session_params: session_params(), url: binary()}} | {:error, term()} + @spec authorize_url(Config.t()) :: on_authorize_url() def authorize_url(config) do config = deprecated_state_handling(config) @@ -209,8 +211,7 @@ defmodule Assent.Strategy.OAuth2 do `authorize_url/1`, optional """ @impl true - @spec callback(Config.t(), map(), atom()) :: - {:ok, %{user: map(), token: map()}} | {:error, term()} + @spec callback(Config.t(), map(), atom()) :: on_callback() def callback(config, params, strategy \\ __MODULE__) do with {:ok, session_params} <- Config.fetch(config, :session_params), :ok <- check_error_params(params), diff --git a/lib/assent/strategies/oauth2/base.ex b/lib/assent/strategies/oauth2/base.ex index baeb013..5ae3d01 100644 --- a/lib/assent/strategies/oauth2/base.ex +++ b/lib/assent/strategies/oauth2/base.ex @@ -59,16 +59,14 @@ defmodule Assent.Strategy.OAuth2.Base do end end - @spec authorize_url(Keyword.t(), module()) :: - {:ok, %{session_params: %{state: binary()}, url: binary()}} + @spec authorize_url(Keyword.t(), module()) :: OAuth2.on_authorize_url() def authorize_url(config, strategy) do config |> set_config(strategy) |> OAuth2.authorize_url() end - @spec callback(Keyword.t(), map(), module()) :: - {:ok, %{user: map(), token: map()}} | {:error, term()} + @spec callback(Keyword.t(), map(), module()) :: OAuth2.on_callback() def callback(config, params, strategy) do config = set_config(config, strategy) diff --git a/lib/assent/strategies/oidc.ex b/lib/assent/strategies/oidc.ex index 1167118..d61fa3e 100644 --- a/lib/assent/strategies/oidc.ex +++ b/lib/assent/strategies/oidc.ex @@ -87,6 +87,17 @@ defmodule Assent.Strategy.OIDC do UnexpectedResponseError } + @type session_params :: %{ + optional(:state) => binary(), + optional(:code_verifier) => binary(), + optional(:code_challenge) => binary(), + optional(:code_challenge_method) => binary(), + optional(:nonce) => binary() + } + + @type on_authorize_url :: OAuth2.on_authorize_url() + @type on_callback :: OAuth2.on_callback() + @doc """ Generates an authorization URL for request phase. @@ -103,13 +114,7 @@ defmodule Assent.Strategy.OIDC do See `Assent.Strategy.OAuth2.authorize_url/1` for more. """ @impl true - @spec authorize_url(Config.t()) :: - {:ok, - %{ - session_params: %{state: binary()} | %{state: binary(), nonce: binary()}, - url: binary() - }} - | {:error, term()} + @spec authorize_url(Config.t()) :: on_authorize_url() def authorize_url(config) do with {:ok, openid_config} <- openid_configuration(config), {:ok, authorize_url} <- @@ -225,8 +230,7 @@ defmodule Assent.Strategy.OIDC do See `Assent.Strategy.OAuth2.callback/3` for more. """ @impl true - @spec callback(Config.t(), map(), atom()) :: - {:ok, %{user: map(), token: map()}} | {:error, term()} + @spec callback(Config.t(), map(), atom()) :: on_callback() def callback(config, params, strategy \\ __MODULE__) do with {:ok, openid_config} <- openid_configuration(config), {:ok, method} <- fetch_client_authentication_method(openid_config, config), @@ -332,12 +336,23 @@ defmodule Assent.Strategy.OIDC do end defp peek_header(encoded, config) do - with [header, _, _] <- String.split(encoded, "."), - {:ok, json} <- Base.url_decode64(header, padding: false) do + with {:ok, header} <- split_header(encoded), + {:ok, json} <- decode_base64_url(header) do Config.json_library(config).decode(json) - else - {:error, error} -> {:error, error} - _any -> {:error, "The ID Token is not a valid JWT"} + end + end + + defp split_header(encoded) do + case String.split(encoded, ".") do + [header, _, _] -> {:ok, header} + _ -> {:error, "The ID Token is not a valid JWT"} + end + end + + defp decode_base64_url(encoded) do + case Base.url_decode64(encoded, padding: false) do + {:ok, decoded} -> {:ok, decoded} + :error -> {:error, "Invalid Base64URL"} end end diff --git a/lib/assent/strategies/oidc/base.ex b/lib/assent/strategies/oidc/base.ex index 78de2e3..664e994 100644 --- a/lib/assent/strategies/oidc/base.ex +++ b/lib/assent/strategies/oidc/base.ex @@ -48,21 +48,14 @@ defmodule Assent.Strategy.OIDC.Base do end end - @spec authorize_url(Keyword.t(), module()) :: - {:ok, - %{ - session_params: %{state: binary()} | %{state: binary(), nonce: binary()}, - url: binary() - }} - | {:error, term()} + @spec authorize_url(Keyword.t(), module()) :: OIDC.on_authorize_url() def authorize_url(config, strategy) do config |> set_config(strategy) |> OIDC.authorize_url() end - @spec callback(Keyword.t(), map(), module()) :: - {:ok, %{user: map(), token: map()}} | {:error, term()} + @spec callback(Keyword.t(), map(), module()) :: OIDC.on_callback() def callback(config, params, strategy) do config = set_config(config, strategy) diff --git a/test/assent/strategies/oidc_test.exs b/test/assent/strategies/oidc_test.exs index 1144262..6aa3254 100644 --- a/test/assent/strategies/oidc_test.exs +++ b/test/assent/strategies/oidc_test.exs @@ -370,6 +370,11 @@ defmodule Assent.Strategy.OIDCTest do {:error, "The ID Token is not a valid JWT"} end + test "with invalid base64 header in id_token", %{config: config} do + assert OIDC.validate_id_token(config, "@invalid.payload.signature") == + {:error, "Invalid Base64URL"} + end + test "with no `:client_secret`", %{config: config, id_token: id_token} do config = Keyword.delete(config, :client_secret)