Current section
19 Versions
Jump to
Current section
19 Versions
Compare versions
5
files changed
+91
additions
-29
deletions
| @@ -5,26 +5,26 @@ | |
| 5 5 | {<<"elixir">>,<<"~> 1.13">>}. |
| 6 6 | {<<"files">>, |
| 7 7 | [<<"lib">>,<<"lib/charon_oauth2">>,<<"lib/charon_oauth2/types">>, |
| 8 | - <<"lib/charon_oauth2/types/encrypted.ex">>, |
| 9 8 | <<"lib/charon_oauth2/types/hmac.ex">>, |
| 9 | + <<"lib/charon_oauth2/types/encrypted.ex">>, |
| 10 10 | <<"lib/charon_oauth2/types/separated_string_ordset.ex">>, |
| 11 | - <<"lib/charon_oauth2/migration.ex">>,<<"lib/charon_oauth2/internal.ex">>, |
| 12 | - <<"lib/charon_oauth2/config.ex">>,<<"lib/charon_oauth2/internal">>, |
| 13 | - <<"lib/charon_oauth2/internal/plug.ex">>, |
| 14 | - <<"lib/charon_oauth2/internal/gen_mod">>, |
| 15 | - <<"lib/charon_oauth2/internal/gen_mod/authorizations.ex">>, |
| 16 | - <<"lib/charon_oauth2/internal/gen_mod/plugs">>, |
| 17 | - <<"lib/charon_oauth2/internal/gen_mod/plugs/token_endpoint.ex">>, |
| 18 | - <<"lib/charon_oauth2/internal/gen_mod/plugs/authorization_endpoint.ex">>, |
| 19 | - <<"lib/charon_oauth2/internal/gen_mod/test_seeds.ex">>, |
| 20 | - <<"lib/charon_oauth2/internal/gen_mod/grants.ex">>, |
| 21 | - <<"lib/charon_oauth2/internal/gen_mod/clients.ex">>, |
| 22 | - <<"lib/charon_oauth2/internal/gen_mod/authorization.ex">>, |
| 23 | - <<"lib/charon_oauth2/internal/gen_mod/grant.ex">>, |
| 24 | - <<"lib/charon_oauth2/internal/gen_mod/client.ex">>, |
| 11 | + <<"lib/charon_oauth2/internal">>, |
| 25 12 | <<"lib/charon_oauth2/internal/req_validators">>, |
| 26 13 | <<"lib/charon_oauth2/internal/req_validators/token_validator.ex">>, |
| 27 14 | <<"lib/charon_oauth2/internal/req_validators/authorization_validator.ex">>, |
| 15 | + <<"lib/charon_oauth2/internal/gen_mod">>, |
| 16 | + <<"lib/charon_oauth2/internal/gen_mod/plugs">>, |
| 17 | + <<"lib/charon_oauth2/internal/gen_mod/plugs/token_endpoint.ex">>, |
| 18 | + <<"lib/charon_oauth2/internal/gen_mod/plugs/authorization_endpoint.ex">>, |
| 19 | + <<"lib/charon_oauth2/internal/gen_mod/grants.ex">>, |
| 20 | + <<"lib/charon_oauth2/internal/gen_mod/grant.ex">>, |
| 21 | + <<"lib/charon_oauth2/internal/gen_mod/test_seeds.ex">>, |
| 22 | + <<"lib/charon_oauth2/internal/gen_mod/client.ex">>, |
| 23 | + <<"lib/charon_oauth2/internal/gen_mod/authorization.ex">>, |
| 24 | + <<"lib/charon_oauth2/internal/gen_mod/clients.ex">>, |
| 25 | + <<"lib/charon_oauth2/internal/gen_mod/authorizations.ex">>, |
| 26 | + <<"lib/charon_oauth2/internal/plug.ex">>,<<"lib/charon_oauth2/config.ex">>, |
| 27 | + <<"lib/charon_oauth2/internal.ex">>,<<"lib/charon_oauth2/migration.ex">>, |
| 28 28 | <<"lib/charon_oauth2.ex">>,<<".formatter.exs">>,<<"mix.exs">>, |
| 29 29 | <<"README.md">>]}. |
| 30 30 | {<<"licenses">>,[<<"Apache-2.0">>]}. |
| @@ -52,4 +52,4 @@ | |
| 52 52 | {<<"optional">>,false}, |
| 53 53 | {<<"repository">>,<<"hexpm">>}, |
| 54 54 | {<<"requirement">>,<<"~> 3.1">>}]]}. |
| 55 | - {<<"version">>,<<"0.5.1">>}. |
| 55 | + {<<"version">>,<<"0.6.0">>}. |
| @@ -42,7 +42,7 @@ defmodule CharonOauth2.Internal.GenMod.Client do | |
| 42 42 | @app_scopes @mod_config.scopes |> :ordsets.from_list() |
| 43 43 | |
| 44 44 | @client_types ~w(confidential public) |
| 45 | - @grant_types ~w(authorization_code refresh_token) |> :ordsets.from_list() |
| 45 | + @grant_types ~w(authorization_code refresh_token client_credentials) |> :ordsets.from_list() |
| 46 46 | @secret_bytesize 48 |
| 47 47 | @autogen_secret {Crypto, :random_url_encoded, [@secret_bytesize]} |
| 48 48 | |
| @@ -96,6 +96,15 @@ defmodule CharonOauth2.Internal.GenMod.Client do | |
| 96 96 | @app_scopes, |
| 97 97 | "must be subset of #{Enum.join(@app_scopes, ", ")}" |
| 98 98 | ) |
| 99 | + |> then(fn cs -> |
| 100 | + grant_types = get_field(cs, :grant_types) || [] |
| 101 | + |
| 102 | + if get_field(cs, :client_type) == "public" and "client_credentials" in grant_types do |
| 103 | + add_error(cs, :grant_types, "client_credentials is not allowed for public clients") |
| 104 | + else |
| 105 | + cs |
| 106 | + end |
| 107 | + end) |
| 99 108 | |> prepare_changes(fn |
| 100 109 | cs = %{data: %{id: id, scope: current_scopes}, changes: %{scope: scopes}} |
| 101 110 | when not is_nil(id) -> |
| @@ -128,6 +128,38 @@ defmodule CharonOauth2.Internal.GenMod.Plugs.TokenEndpoint do | |
| 128 128 | # Private # |
| 129 129 | ########### |
| 130 130 | |
| 131 | + defp process_grant( |
| 132 | + cs = %{changes: %{grant_type: "client_credentials", client: client}}, |
| 133 | + conn, |
| 134 | + opts |
| 135 | + ) do |
| 136 | + dummy_auth = %{scope: client.scope, client: client, client_id: client.owner_id} |
| 137 | + |
| 138 | + with cs = %{valid?: true} <- Validate.client_credentials_flow(cs, dummy_auth) do |
| 139 | + scopes = Map.get(cs.changes, :scope, dummy_auth.scope) |
| 140 | + |
| 141 | + conn |
| 142 | + |> upsert_session(dummy_auth, scopes, opts, user_id: client.id) |
| 143 | + |> send_token_response(scopes, now(), opts, false) |
| 144 | + else |
| 145 | + invalid_cs -> |
| 146 | + error_map = changeset_errors_to_map(invalid_cs) |
| 147 | + descr = error_map |> cs_error_map_to_string() |
| 148 | + |
| 149 | + error_map |
| 150 | + |> case do |
| 151 | + %{scope: ["user authorized " <> _]} -> |
| 152 | + json_error(conn, 400, "invalid_scope", descr, opts) |
| 153 | + |
| 154 | + %{grant_type: ["unsupported by client"]} -> |
| 155 | + json_error(conn, 400, "unauthorized_client", descr, opts) |
| 156 | + |
| 157 | + other -> |
| 158 | + json_error(conn, 400, "invalid_request", descr, opts) |
| 159 | + end |
| 160 | + end |
| 161 | + end |
| 162 | + |
| 131 163 | defp process_grant(cs = %{changes: %{grant_type: "authorization_code"}}, conn, opts) do |
| 132 164 | with cs = %{valid?: true, changes: %{code: code}} <- |
| 133 165 | Validate.authorization_code_flow_step_1(cs), |
| @@ -143,7 +175,7 @@ defmodule CharonOauth2.Internal.GenMod.Plugs.TokenEndpoint do | |
| 143 175 | |
| 144 176 | conn |
| 145 177 | |> upsert_session(authorization, scopes, opts, user_id: grant.resource_owner_id) |
| 146 | - |> send_token_response(scopes, now, opts) |
| 178 | + |> send_token_response(scopes, now, opts, true) |
| 147 179 | else |
| 148 180 | # https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 |
| 149 181 | # grant not found |
| @@ -196,10 +228,8 @@ defmodule CharonOauth2.Internal.GenMod.Plugs.TokenEndpoint do | |
| 196 228 | |
| 197 229 | conn |
| 198 230 | |> upsert_session(authorization, scopes, opts) |
| 199 | - |> send_token_response(scopes, now(), opts) |
| 200 | - end |
| 201 | - # this ugly crap instead of with-else is needed to keep dialyzer happy :| |
| 202 | - |> case do |
| 231 | + |> send_token_response(scopes, now(), opts, true) |
| 232 | + else |
| 203 233 | conn = %Plug.Conn{} -> |
| 204 234 | conn |
| 205 235 | |
| @@ -255,10 +285,13 @@ defmodule CharonOauth2.Internal.GenMod.Plugs.TokenEndpoint do | |
| 255 285 | |> then(&apply(SessionPlugs, :upsert_session, &1)) |
| 256 286 | end |
| 257 287 | |
| 258 | - defp send_token_response(conn, scopes, now, opts) do |
| 259 | - tokens = conn |> Utils.get_tokens() |
| 288 | + defp send_token_response(conn, scopes, now, opts, incl_refresh_token) do |
| 289 | + resp_body = conn |> Utils.get_tokens() |> resp_body(scopes, now, incl_refresh_token) |
| 290 | + json(conn, 200, resp_body, opts) |
| 291 | + end |
| 260 292 | |
| 261 | - resp_body = %{ |
| 293 | + defp resp_body(tokens, scopes, now, _incl_refresh_token = true) do |
| 294 | + %{ |
| 262 295 | access_token: tokens.access_token, |
| 263 296 | expires_in: tokens.access_token_exp - now, |
| 264 297 | refresh_expires_in: tokens.refresh_token_exp - now, |
| @@ -266,8 +299,15 @@ defmodule CharonOauth2.Internal.GenMod.Plugs.TokenEndpoint do | |
| 266 299 | scope: scopes |> Enum.join(" "), |
| 267 300 | token_type: "bearer" |
| 268 301 | } |
| 302 | + end |
| 269 303 | |
| 270 | - json(conn, 200, resp_body, opts) |
| 304 | + defp resp_body(tokens, scopes, now, _) do |
| 305 | + %{ |
| 306 | + access_token: tokens.access_token, |
| 307 | + expires_in: tokens.access_token_exp - now, |
| 308 | + scope: scopes |> Enum.join(" "), |
| 309 | + token_type: "bearer" |
| 310 | + } |
| 271 311 | end |
| 272 312 | |
| 273 313 | defp add_cors_headers(conn) do |
| @@ -23,7 +23,7 @@ defmodule CharonOauth2.Internal.TokenValidator do | |
| 23 23 | field :scope, SeparatedStringOrdset, pattern: [" ", ","] |
| 24 24 | end |
| 25 25 | |
| 26 | - @grant_types ~w(authorization_code refresh_token) |
| 26 | + @grant_types ~w(authorization_code refresh_token client_credentials) |
| 27 27 | |
| 28 28 | @doc """ |
| 29 29 | All parameters must pass type validation or return an "invalid_request" response. |
| @@ -119,6 +119,19 @@ defmodule CharonOauth2.Internal.TokenValidator do | |
| 119 119 | |> validate_scope(authorization) |
| 120 120 | end |
| 121 121 | |
| 122 | + @doc """ |
| 123 | + After authenticating the client, we validate that: |
| 124 | + - the grant type is enabled for the client |
| 125 | + - the requested scope is valid for the client |
| 126 | + |
| 127 | + The "authorization" is a bit of a dummy (there's no user to authorize anything in the client_credentials flow), |
| 128 | + and only represents configuration of the client itself (grant types, scopes). |
| 129 | + """ |
| 130 | + @spec client_credentials_flow(Changeset.t(), map()) :: Changeset.t() |
| 131 | + def client_credentials_flow(cs, authorization) do |
| 132 | + cs |> validate_client_grant_type(authorization.client) |> validate_scope(authorization) |
| 133 | + end |
| 134 | + |
| 122 135 | @doc """ |
| 123 136 | For the refresh token flow, we verify that a token is present and that the client |
| 124 137 | supports the grant type. |
| @@ -4,7 +4,7 @@ defmodule CharonOauth2.MixProject do | |
| 4 4 | def project do |
| 5 5 | [ |
| 6 6 | app: :charon_oauth2, |
| 7 | - version: "0.5.1", |
| 7 | + version: "0.6.0", |
| 8 8 | elixir: "~> 1.13", |
| 9 9 | start_permanent: Mix.env() == :prod, |
| 10 10 | deps: deps(), |
| @@ -21,7 +21,7 @@ defmodule CharonOauth2.MixProject do | |
| 21 21 | source_url: "https://github.com/weareyipyip/charon_oauth2", |
| 22 22 | name: "CharonOauth2", |
| 23 23 | docs: [ |
| 24 | - source_ref: "v0.5.1", |
| 24 | + source_ref: "v0.6.0", |
| 25 25 | extras: ["./README.md", "./example-auth-page.md"], |
| 26 26 | main: "readme" |
| 27 27 | ] |