Current section

19 Versions

Jump to

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 ]