Ver código fonte

Merge pull request #6316 from savonarola/test-authn-resources-jwt

chore(authn): add JWKS backend tests
Ilya Averyanov 4 anos atrás
pai
commit
4941b4d1a0

+ 30 - 25
apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl

@@ -20,6 +20,8 @@
 
 -include_lib("emqx/include/logger.hrl").
 -include_lib("jose/include/jose_jwk.hrl").
+-include_lib("snabbkaffe/include/snabbkaffe.hrl").
+
 
 -export([ start_link/1
         , stop/1
@@ -66,9 +68,9 @@ init([Opts]) ->
 handle_call(get_cached_jwks, _From, #{jwks := Jwks} = State) ->
     {reply, {ok, Jwks}, State};
 
-handle_call({update, Opts}, _From, State) ->
-    State = handle_options(Opts),
-    {reply, ok, refresh_jwks(State)};
+handle_call({update, Opts}, _From, _State) ->
+    NewState = handle_options(Opts),
+    {reply, ok, refresh_jwks(NewState)};
 
 handle_call(_Req, _From, State) ->
     {reply, ok, State}.
@@ -91,25 +93,27 @@ handle_info({refresh_jwks, _TRef, refresh}, #{request_id := RequestID} = State)
 
 handle_info({http, {RequestID, Result}},
             #{request_id := RequestID, endpoint := Endpoint} = State0) ->
+    ?tp(debug, jwks_endpoint_response, #{request_id => RequestID}),
     State1 = State0#{request_id := undefined},
-    case Result of
-        {error, Reason} ->
-            ?SLOG(warning, #{msg => "failed_to_request_jwks_endpoint",
-                             endpoint => Endpoint,
-                             reason => Reason}),
-            State1;
-        {_StatusLine, _Headers, Body} ->
-            try
-                JWKS = jose_jwk:from(emqx_json:decode(Body, [return_maps])),
-                {_, JWKs} = JWKS#jose_jwk.keys,
-                State1#{jwks := JWKs}
-            catch _:_ ->
-                ?SLOG(warning, #{msg => "invalid_jwks_returned",
-                                 endpoint => Endpoint,
-                                 body => Body}),
-                State1
-            end
-    end;
+    NewState = case Result of
+                   {error, Reason} ->
+                       ?SLOG(warning, #{msg => "failed_to_request_jwks_endpoint",
+                                        endpoint => Endpoint,
+                                        reason => Reason}),
+                       State1;
+                   {_StatusLine, _Headers, Body} ->
+                       try
+                           JWKS = jose_jwk:from(emqx_json:decode(Body, [return_maps])),
+                           {_, JWKs} = JWKS#jose_jwk.keys,
+                           State1#{jwks := JWKs}
+                       catch _:_ ->
+                                 ?SLOG(warning, #{msg => "invalid_jwks_returned",
+                                                  endpoint => Endpoint,
+                                                  body => Body}),
+                                 State1
+                       end
+               end,
+    {noreply, NewState};
 
 handle_info({http, {_, _}}, State) ->
     %% ignore
@@ -147,17 +151,18 @@ refresh_jwks(#{endpoint := Endpoint,
     NState = case httpc:request(get, {Endpoint, [{"Accept", "application/json"}]}, HTTPOpts,
                                 [{body_format, binary}, {sync, false}, {receiver, self()}]) of
                  {error, Reason} ->
-                     ?SLOG(warning, #{msg => "failed_to_request_jwks_endpoint",
-                                      endpoint => Endpoint,
-                                      reason => Reason}),
+                     ?tp(warning, jwks_endpoint_request_fail, #{endpoint => Endpoint,
+                                                                http_opts => HTTPOpts,
+                                                                reason => Reason}),
                      State;
                  {ok, RequestID} ->
+                     ?tp(debug, jwks_endpoint_request_ok, #{request_id => RequestID}),
                      State#{request_id := RequestID}
              end,
     ensure_expiry_timer(NState).
 
 ensure_expiry_timer(State = #{refresh_interval := Interval}) ->
-    State#{refresh_timer := emqx_misc:start_timer(timer:seconds(Interval), refresh_jwks)}.
+    State#{refresh_timer => emqx_misc:start_timer(timer:seconds(Interval), refresh_jwks)}.
 
 cancel_timer(State = #{refresh_timer := undefined}) ->
     State;

+ 13 - 10
apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl

@@ -157,7 +157,7 @@ update(#{use_jwks := false} = Config, _State) ->
 update(#{use_jwks := true} = Config,
        #{jwk := Connector} = State)
   when is_pid(Connector) ->
-    ok = emqx_authn_jwks_connector:update(Connector, Config),
+    ok = emqx_authn_jwks_connector:update(Connector, connector_opts(Config)),
     case maps:get(verify_cliams, Config, undefined) of
         undefined ->
             {ok, State};
@@ -208,7 +208,7 @@ create2(#{use_jwks := false,
             JWK = jose_jwk:from_oct(Secret),
             {ok, #{jwk => JWK,
                    verify_claims => VerifyClaims}}
-    end;                                                                                           
+    end;
 
 create2(#{use_jwks := false,
           algorithm := 'public-key',
@@ -219,13 +219,8 @@ create2(#{use_jwks := false,
            verify_claims => VerifyClaims}};
 
 create2(#{use_jwks := true,
-          verify_claims := VerifyClaims,
-          ssl := #{enable := Enable} = SSL} = Config) ->
-    SSLOpts = case Enable of
-                  true -> maps:without([enable], SSL);
-                  false -> #{}
-              end,
-    case emqx_authn_jwks_connector:start_link(Config#{ssl_opts => SSLOpts}) of
+          verify_claims := VerifyClaims} = Config) ->
+    case emqx_authn_jwks_connector:start_link(connector_opts(Config)) of
         {ok, Connector} ->
             {ok, #{jwk => Connector,
                    verify_claims => VerifyClaims}};
@@ -233,6 +228,14 @@ create2(#{use_jwks := true,
             {error, Reason}
     end.
 
+connector_opts(#{ssl := #{enable := Enable} = SSL} = Config) ->
+    SSLOpts = case Enable of
+                  true -> maps:without([enable], SSL);
+                  false -> #{}
+              end,
+    Config#{ssl_opts => SSLOpts}.
+
+
 may_decode_secret(false, Secret) -> Secret;
 may_decode_secret(true, Secret) ->
     try base64:decode(Secret)
@@ -260,7 +263,7 @@ verify(JWS, [JWK | More], VerifyClaims) ->
             Claims = emqx_json:decode(Payload, [return_maps]),
             case verify_claims(Claims, VerifyClaims) of
                 ok ->
-                    {ok, #{is_superuser => maps:get(<<"is_superuser">>, Claims, false)}};
+                    {ok, emqx_authn_utils:is_superuser(Claims)};
                 {error, Reason} ->
                     {error, Reason}
             end;

+ 85 - 5
apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl

@@ -21,11 +21,16 @@
 
 -include_lib("common_test/include/ct.hrl").
 -include_lib("eunit/include/eunit.hrl").
+-include_lib("snabbkaffe/include/snabbkaffe.hrl").
 
 -include("emqx_authn.hrl").
 
 -define(AUTHN_ID, <<"mechanism:jwt">>).
 
+-define(JWKS_PORT, 33333).
+-define(JWKS_PATH, "/jwks.json").
+
+
 all() ->
     emqx_common_test_helpers:all(?MODULE).
 
@@ -37,7 +42,11 @@ end_per_suite(_) ->
     emqx_common_test_helpers:stop_apps([emqx_authn]),
     ok.
 
-t_jwt_authenticator(_) ->
+%%------------------------------------------------------------------------------
+%% Tests
+%%------------------------------------------------------------------------------
+
+t_jwt_authenticator_hmac_based(_) ->
     Secret = <<"abcdef">>,
     Config = #{mechanism => jwt,
                use_jwks => false,
@@ -121,10 +130,9 @@ t_jwt_authenticator(_) ->
     ?assertEqual(ok, emqx_authn_jwt:destroy(State3)),
     ok.
 
-t_jwt_authenticator2(_) ->
-    Dir = code:lib_dir(emqx_authn, test),
-    PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])),
-    PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])),
+t_jwt_authenticator_public_key(_) ->
+    PublicKey = test_rsa_key(public),
+    PrivateKey = test_rsa_key(private),
     Config = #{mechanism => jwt,
                use_jwks => false,
                algorithm => 'public-key',
@@ -142,6 +150,78 @@ t_jwt_authenticator2(_) ->
     ?assertEqual(ok, emqx_authn_jwt:destroy(State)),
     ok.
 
+t_jwks_renewal(_Config) ->
+    ok = emqx_authn_http_test_server:start(?JWKS_PORT, ?JWKS_PATH),
+    ok = emqx_authn_http_test_server:set_handler(fun jwks_handler/2),
+
+    PrivateKey = test_rsa_key(private),
+    Payload = #{<<"username">> => <<"myuser">>},
+    JWS = generate_jws('public-key', Payload, PrivateKey),
+    Credential = #{username => <<"myuser">>,
+			       password => JWS},
+
+    BadConfig = #{mechanism => jwt,
+                  algorithm => 'public-key',
+                  ssl => #{enable => false},
+                  verify_claims => [],
+
+                  use_jwks => true,
+                  endpoint => "http://127.0.0.1:" ++ integer_to_list(?JWKS_PORT + 1) ++ ?JWKS_PATH,
+                  refresh_interval => 1000
+                 },
+
+    ok = snabbkaffe:start_trace(),
+
+    {{ok, State0}, _} = ?wait_async_action(
+                           emqx_authn_jwt:create(?AUTHN_ID, BadConfig),
+                           #{?snk_kind := jwks_endpoint_response},
+                           1000),
+
+    ok = snabbkaffe:stop(),
+
+    ?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential, State0)),
+    ?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State0)),
+
+    GoodConfig = BadConfig#{endpoint =>
+                            "http://127.0.0.1:" ++ integer_to_list(?JWKS_PORT) ++ ?JWKS_PATH},
+
+    ok = snabbkaffe:start_trace(),
+
+    {{ok, State1}, _} = ?wait_async_action(
+                           emqx_authn_jwt:update(GoodConfig, State0),
+                           #{?snk_kind := jwks_endpoint_response},
+                           1000),
+
+    ok = snabbkaffe:stop(),
+
+    ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State1)),
+    ?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State1)),
+
+    ?assertEqual(ok, emqx_authn_jwt:destroy(State1)),
+    ok = emqx_authn_http_test_server:stop().
+
+%%------------------------------------------------------------------------------
+%% Helpers
+%%------------------------------------------------------------------------------
+
+jwks_handler(Req0, State) ->
+    JWK = jose_jwk:from_pem_file(test_rsa_key(public)),
+    JWKS = jose_jwk_set:to_map([JWK], #{}),
+    Req = cowboy_req:reply(
+            200,
+            #{<<"content-type">> => <<"application/json">>},
+            jiffy:encode(JWKS),
+            Req0),
+    {ok, Req, State}.
+
+test_rsa_key(public) ->
+    Dir = code:lib_dir(emqx_authn, test),
+    list_to_binary(filename:join([Dir, "data/public_key.pem"]));
+
+test_rsa_key(private) ->
+    Dir = code:lib_dir(emqx_authn, test),
+    list_to_binary(filename:join([Dir, "data/private_key.pem"])).
+
 generate_jws('hmac-based', Payload, Secret) ->
     JWK = jose_jwk:from_oct(Secret),
     Header = #{ <<"alg">> => <<"HS256">>