Merge pull request #7939 from savonarola/authz-generalize

feat(authz): add default authn-based authz source
This commit is contained in:
Ilya Averyanov 2022-05-13 17:12:56 +03:00 committed by GitHub
commit 4d661cd67b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 214 additions and 229 deletions

View File

@ -210,4 +210,15 @@ Authentication will verify that the value of claims in the JWT (taken from the P
zh: """SSL 配置。""" zh: """SSL 配置。"""
} }
} }
acl_claim_name {
desc {
en: """JWT claim name to use for getting ACL rules."""
zh: """JWT claim name to use for getting ACL rules."""
}
label {
en: """acl_claim_name"""
zh: """acl_claim_name"""
}
}
} }

View File

@ -111,6 +111,11 @@ desc(_) ->
common_fields() -> common_fields() ->
[ [
{mechanism, emqx_authn_schema:mechanism('jwt')}, {mechanism, emqx_authn_schema:mechanism('jwt')},
{acl_claim_name, #{
type => binary(),
default => <<"acl">>,
desc => ?DESC(acl_claim_name)
}},
{verify_claims, fun verify_claims/1} {verify_claims, fun verify_claims/1}
] ++ emqx_authn_schema:common_fields(). ] ++ emqx_authn_schema:common_fields().
@ -231,17 +236,19 @@ authenticate(
Credential = #{password := JWT}, Credential = #{password := JWT},
#{ #{
verify_claims := VerifyClaims0, verify_claims := VerifyClaims0,
jwk := JWK jwk := JWK,
acl_claim_name := AclClaimName
} }
) -> ) ->
JWKs = [JWK], JWKs = [JWK],
VerifyClaims = replace_placeholder(VerifyClaims0, Credential), VerifyClaims = replace_placeholder(VerifyClaims0, Credential),
verify(JWT, JWKs, VerifyClaims); verify(JWT, JWKs, VerifyClaims, AclClaimName);
authenticate( authenticate(
Credential = #{password := JWT}, Credential = #{password := JWT},
#{ #{
verify_claims := VerifyClaims0, verify_claims := VerifyClaims0,
jwk_resource := ResourceId jwk_resource := ResourceId,
acl_claim_name := AclClaimName
} }
) -> ) ->
case emqx_resource:query(ResourceId, get_jwks) of case emqx_resource:query(ResourceId, get_jwks) of
@ -254,7 +261,7 @@ authenticate(
ignore; ignore;
{ok, JWKs} -> {ok, JWKs} ->
VerifyClaims = replace_placeholder(VerifyClaims0, Credential), VerifyClaims = replace_placeholder(VerifyClaims0, Credential),
verify(JWT, JWKs, VerifyClaims) verify(JWT, JWKs, VerifyClaims, AclClaimName)
end. end.
destroy(#{jwk_resource := ResourceId}) -> destroy(#{jwk_resource := ResourceId}) ->
@ -272,7 +279,8 @@ create2(#{
algorithm := 'hmac-based', algorithm := 'hmac-based',
secret := Secret0, secret := Secret0,
secret_base64_encoded := Base64Encoded, secret_base64_encoded := Base64Encoded,
verify_claims := VerifyClaims verify_claims := VerifyClaims,
acl_claim_name := AclClaimName
}) -> }) ->
case may_decode_secret(Base64Encoded, Secret0) of case may_decode_secret(Base64Encoded, Secret0) of
{error, Reason} -> {error, Reason} ->
@ -281,24 +289,28 @@ create2(#{
JWK = jose_jwk:from_oct(Secret), JWK = jose_jwk:from_oct(Secret),
{ok, #{ {ok, #{
jwk => JWK, jwk => JWK,
verify_claims => VerifyClaims verify_claims => VerifyClaims,
acl_claim_name => AclClaimName
}} }}
end; end;
create2(#{ create2(#{
use_jwks := false, use_jwks := false,
algorithm := 'public-key', algorithm := 'public-key',
public_key := PublicKey, public_key := PublicKey,
verify_claims := VerifyClaims verify_claims := VerifyClaims,
acl_claim_name := AclClaimName
}) -> }) ->
JWK = create_jwk_from_public_key(PublicKey), JWK = create_jwk_from_public_key(PublicKey),
{ok, #{ {ok, #{
jwk => JWK, jwk => JWK,
verify_claims => VerifyClaims verify_claims => VerifyClaims,
acl_claim_name => AclClaimName
}}; }};
create2( create2(
#{ #{
use_jwks := true, use_jwks := true,
verify_claims := VerifyClaims verify_claims := VerifyClaims,
acl_claim_name := AclClaimName
} = Config } = Config
) -> ) ->
ResourceId = emqx_authn_utils:make_resource_id(?MODULE), ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
@ -310,7 +322,8 @@ create2(
), ),
{ok, #{ {ok, #{
jwk_resource => ResourceId, jwk_resource => ResourceId,
verify_claims => VerifyClaims verify_claims => VerifyClaims,
acl_claim_name => AclClaimName
}}. }}.
create_jwk_from_public_key(PublicKey) when create_jwk_from_public_key(PublicKey) when
@ -352,23 +365,39 @@ replace_placeholder([{Name, {placeholder, PL}} | More], Variables, Acc) ->
replace_placeholder([{Name, Value} | More], Variables, Acc) -> replace_placeholder([{Name, Value} | More], Variables, Acc) ->
replace_placeholder(More, Variables, [{Name, Value} | Acc]). replace_placeholder(More, Variables, [{Name, Value} | Acc]).
verify(JWT, JWKs, VerifyClaims) -> verify(JWT, JWKs, VerifyClaims, AclClaimName) ->
case do_verify(JWT, JWKs, VerifyClaims) of case do_verify(JWT, JWKs, VerifyClaims) of
{ok, Extra} -> {ok, Extra}; {ok, Extra} -> {ok, acl(Extra, AclClaimName)};
{error, {missing_claim, _}} -> {error, bad_username_or_password}; {error, {missing_claim, _}} -> {error, bad_username_or_password};
{error, invalid_signature} -> ignore; {error, invalid_signature} -> ignore;
{error, {claims, _}} -> {error, bad_username_or_password} {error, {claims, _}} -> {error, bad_username_or_password}
end. end.
acl(Claims, AclClaimName) ->
Acl =
case Claims of
#{<<"exp">> := Expire, AclClaimName := Rules} ->
#{
acl => #{
rules => Rules,
expire => Expire
}
};
_ ->
#{}
end,
maps:merge(emqx_authn_utils:is_superuser(Claims), Acl).
do_verify(_JWS, [], _VerifyClaims) -> do_verify(_JWS, [], _VerifyClaims) ->
{error, invalid_signature}; {error, invalid_signature};
do_verify(JWS, [JWK | More], VerifyClaims) -> do_verify(JWS, [JWK | More], VerifyClaims) ->
try jose_jws:verify(JWK, JWS) of try jose_jws:verify(JWK, JWS) of
{true, Payload, _JWS} -> {true, Payload, _JWS} ->
Claims = emqx_json:decode(Payload, [return_maps]), Claims0 = emqx_json:decode(Payload, [return_maps]),
Claims = try_convert_to_int(Claims0, [<<"exp">>, <<"iat">>, <<"nbf">>]),
case verify_claims(Claims, VerifyClaims) of case verify_claims(Claims, VerifyClaims) of
ok -> ok ->
{ok, maps:put(jwt, Claims, emqx_authn_utils:is_superuser(Claims))}; {ok, Claims};
{error, Reason} -> {error, Reason} ->
{error, Reason} {error, Reason}
end; end;
@ -384,37 +413,39 @@ verify_claims(Claims, VerifyClaims0) ->
Now = os:system_time(seconds), Now = os:system_time(seconds),
VerifyClaims = VerifyClaims =
[ [
{<<"exp">>, required, {<<"exp">>, required, fun(ExpireTime) ->
with_int_value(fun(ExpireTime) -> is_integer(ExpireTime) andalso Now < ExpireTime
Now < ExpireTime end},
end)}, {<<"iat">>, optional, fun(IssueAt) ->
{<<"iat">>, optional, is_integer(IssueAt) andalso IssueAt =< Now
with_int_value(fun(IssueAt) -> end},
IssueAt =< Now {<<"nbf">>, optional, fun(NotBefore) ->
end)}, is_integer(NotBefore) andalso NotBefore =< Now
{<<"nbf">>, optional, end}
with_int_value(fun(NotBefore) ->
NotBefore =< Now
end)}
] ++ VerifyClaims0, ] ++ VerifyClaims0,
do_verify_claims(Claims, VerifyClaims). do_verify_claims(Claims, VerifyClaims).
with_int_value(Fun) -> try_convert_to_int(Claims, [Name | Names]) ->
fun(Value) -> case Claims of
case Value of #{Name := Value} ->
Int when is_integer(Int) -> Fun(Int); case Value of
Bin when is_binary(Bin) -> Int when is_integer(Int) ->
case string:to_integer(Bin) of try_convert_to_int(Claims#{Name => Int}, Names);
{Int, <<>>} -> Fun(Int); Bin when is_binary(Bin) ->
_ -> false case string:to_integer(Bin) of
end; {Int, <<>>} ->
Str when is_list(Str) -> try_convert_to_int(Claims#{Name => Int}, Names);
case string:to_integer(Str) of _ ->
{Int, ""} -> Fun(Int); try_convert_to_int(Claims, Names)
_ -> false end;
end _ ->
end try_convert_to_int(Claims, Names)
end. end;
_ ->
try_convert_to_int(Claims, Names)
end;
try_convert_to_int(Claims, []) ->
Claims.
do_verify_claims(_Claims, []) -> do_verify_claims(_Claims, []) ->
ok; ok;

View File

@ -56,6 +56,7 @@ t_jwt_authenticator_hmac_based(_) ->
Secret = <<"abcdef">>, Secret = <<"abcdef">>,
Config = #{ Config = #{
mechanism => jwt, mechanism => jwt,
acl_claim_name => <<"acl">>,
use_jwks => false, use_jwks => false,
algorithm => 'hmac-based', algorithm => 'hmac-based',
secret => Secret, secret => Secret,
@ -179,6 +180,7 @@ t_jwt_authenticator_public_key(_) ->
PrivateKey = test_rsa_key(private), PrivateKey = test_rsa_key(private),
Config = #{ Config = #{
mechanism => jwt, mechanism => jwt,
acl_claim_name => <<"acl">>,
use_jwks => false, use_jwks => false,
algorithm => 'public-key', algorithm => 'public-key',
public_key => PublicKey, public_key => PublicKey,
@ -214,6 +216,7 @@ t_jwks_renewal(_Config) ->
BadConfig0 = #{ BadConfig0 = #{
mechanism => jwt, mechanism => jwt,
acl_claim_name => <<"acl">>,
algorithm => 'public-key', algorithm => 'public-key',
ssl => #{enable => false}, ssl => #{enable => false},
verify_claims => [], verify_claims => [],
@ -308,6 +311,7 @@ t_jwt_authenticator_verify_claims(_) ->
Secret = <<"abcdef">>, Secret = <<"abcdef">>,
Config0 = #{ Config0 = #{
mechanism => jwt, mechanism => jwt,
acl_claim_name => <<"acl">>,
use_jwks => false, use_jwks => false,
algorithm => 'hmac-based', algorithm => 'hmac-based',
secret => Secret, secret => Secret,
@ -384,6 +388,7 @@ t_jwt_not_allow_empty_claim_name(_) ->
Request = #{ Request = #{
<<"use_jwks">> => false, <<"use_jwks">> => false,
<<"algorithm">> => <<"hmac-based">>, <<"algorithm">> => <<"hmac-based">>,
<<"acl_claim_name">> => <<"acl">>,
<<"secret">> => <<"secret">>, <<"secret">> => <<"secret">>,
<<"mechanism">> => <<"jwt">> <<"mechanism">> => <<"jwt">>
}, },

View File

@ -348,28 +348,6 @@ Filter supports the following placeholders:
} }
} }
jwt {
desc {
en: """Authorization using ACL rules from authentication JWT."""
zh: """使用 JWT 登录认证中携带的 ACL 规则来进行发布和订阅的授权。"""
}
label {
en: """jwt"""
zh: """jwt"""
}
}
acl_claim_name {
desc {
en: """JWT claim name to use for getting ACL rules."""
zh: """JWT claim name to use for getting ACL rules."""
}
label {
en: """acl_claim_name"""
zh: """acl_claim_name"""
}
}
cmd { cmd {
desc { desc {
en: """Database query used to retrieve authorization data.""" en: """Database query used to retrieve authorization data."""

View File

@ -95,6 +95,7 @@ register_metrics() ->
init() -> init() ->
ok = register_metrics(), ok = register_metrics(),
ok = init_metrics(client_info_source()),
emqx_conf:add_handler(?CONF_KEY_PATH, ?MODULE), emqx_conf:add_handler(?CONF_KEY_PATH, ?MODULE),
Sources = emqx_conf:get(?CONF_KEY_PATH, []), Sources = emqx_conf:get(?CONF_KEY_PATH, []),
ok = check_dup_types(Sources), ok = check_dup_types(Sources),
@ -307,7 +308,7 @@ authorize(
DefaultResult, DefaultResult,
Sources Sources
) -> ) ->
case do_authorize(Client, PubSub, Topic, Sources) of case do_authorize(Client, PubSub, Topic, sources_with_defaults(Sources)) of
{{matched, allow}, AuthzSource} -> {{matched, allow}, AuthzSource} ->
emqx:run_hook( emqx:run_hook(
'client.check_authz_complete', 'client.check_authz_complete',
@ -392,6 +393,14 @@ get_enabled_authzs() ->
%% Internal function %% Internal function
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
client_info_source() ->
emqx_authz_client_info:create(
#{type => client_info, enable => true}
).
sources_with_defaults(Sources) ->
[client_info_source() | Sources].
take(Type) -> take(Type, lookup()). take(Type) -> take(Type, lookup()).
%% Take the source of give type, the sources list is split into two parts %% Take the source of give type, the sources list is split into two parts
@ -431,8 +440,8 @@ type(postgresql) -> postgresql;
type(<<"postgresql">>) -> postgresql; type(<<"postgresql">>) -> postgresql;
type(built_in_database) -> built_in_database; type(built_in_database) -> built_in_database;
type(<<"built_in_database">>) -> built_in_database; type(<<"built_in_database">>) -> built_in_database;
type(jwt) -> jwt; type(client_info) -> client_info;
type(<<"jwt">>) -> jwt; type(<<"client_info">>) -> client_info;
%% should never happen if the input is type-checked by hocon schema %% should never happen if the input is type-checked by hocon schema
type(Unknown) -> throw({unknown_authz_source_type, Unknown}). type(Unknown) -> throw({unknown_authz_source_type, Unknown}).

View File

@ -0,0 +1,111 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_authz_client_info).
-include_lib("emqx/include/logger.hrl").
-behaviour(emqx_authz).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
%% APIs
-export([
description/0,
create/1,
update/1,
destroy/1,
authorize/4
]).
-define(RULE_NAMES, [
{[pub, <<"pub">>], publish},
{[sub, <<"sub">>], subscribe},
{[all, <<"all">>], all}
]).
%%--------------------------------------------------------------------
%% emqx_authz callbacks
%%--------------------------------------------------------------------
description() ->
"AuthZ with ClientInfo".
create(Source) ->
Source.
update(Source) ->
Source.
destroy(_Source) -> ok.
authorize(#{acl := Acl} = Client, PubSub, Topic, _Source) ->
case check(Acl) of
{ok, Rules} when is_map(Rules) ->
do_authorize(Client, PubSub, Topic, Rules);
{error, MatchResult} ->
MatchResult
end;
authorize(_Client, _PubSub, _Topic, _Source) ->
nomatch.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
check(#{expire := Expire, rules := Rules}) when is_map(Rules) ->
Now = erlang:system_time(second),
case Expire of
N when is_integer(N) andalso N > Now -> {ok, Rules};
undefined -> {ok, Rules};
_ -> {error, {matched, deny}}
end;
%% no expire
check(#{rules := Rules}) ->
{ok, Rules};
%% no rules no match
check(#{}) ->
{error, nomatch}.
do_authorize(Client, PubSub, Topic, AclRules) ->
do_authorize(Client, PubSub, Topic, AclRules, ?RULE_NAMES).
do_authorize(_Client, _PubSub, _Topic, _AclRules, []) ->
{matched, deny};
do_authorize(Client, PubSub, Topic, AclRules, [{Keys, Action} | RuleNames]) ->
TopicFilters = get_topic_filters(Keys, AclRules, []),
case
emqx_authz_rule:match(
Client,
PubSub,
Topic,
emqx_authz_rule:compile({allow, all, Action, TopicFilters})
)
of
{matched, Permission} -> {matched, Permission};
nomatch -> do_authorize(Client, PubSub, Topic, AclRules, RuleNames)
end.
get_topic_filters([], _Rules, Default) ->
Default;
get_topic_filters([Key | Keys], Rules, Default) ->
case Rules of
#{Key := Value} -> Value;
#{} -> get_topic_filters(Keys, Rules, Default)
end.

View File

@ -1,142 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_authz_jwt).
-include_lib("emqx/include/logger.hrl").
-behaviour(emqx_authz).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
%% APIs
-export([
description/0,
create/1,
update/1,
destroy/1,
authorize/4
]).
-define(JWT_RULE_NAMES, [
{<<"pub">>, publish},
{<<"sub">>, subscribe},
{<<"all">>, all}
]).
%%--------------------------------------------------------------------
%% emqx_authz callbacks
%%--------------------------------------------------------------------
description() ->
"AuthZ with JWT".
create(#{acl_claim_name := _AclClaimName} = Source) ->
Source.
update(#{acl_claim_name := _AclClaimName} = Source) ->
Source.
destroy(_Source) -> ok.
authorize(#{jwt := JWT} = Client, PubSub, Topic, #{acl_claim_name := AclClaimName}) ->
case verify(JWT) of
{ok, #{AclClaimName := Rules}} when is_map(Rules) ->
do_authorize(Client, PubSub, Topic, Rules);
_ ->
{matched, deny}
end;
authorize(_Client, _PubSub, _Topic, _Source) ->
nomatch.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
verify(JWT) ->
Now = erlang:system_time(second),
VerifyClaims =
[
{<<"exp">>, required,
with_int_value(fun(ExpireTime) ->
Now < ExpireTime
end)},
{<<"iat">>, optional,
with_int_value(fun(IssueAt) ->
IssueAt =< Now
end)},
{<<"nbf">>, optional,
with_int_value(fun(NotBefore) ->
NotBefore =< Now
end)}
],
IsValid = lists:all(
fun({ClaimName, Required, Validator}) ->
verify_claim(ClaimName, Required, JWT, Validator)
end,
VerifyClaims
),
case IsValid of
true -> {ok, JWT};
false -> error
end.
with_int_value(Fun) ->
fun(Value) ->
case Value of
Int when is_integer(Int) -> Fun(Int);
Bin when is_binary(Bin) ->
case string:to_integer(Bin) of
{Int, <<>>} -> Fun(Int);
_ -> false
end;
Str when is_list(Str) ->
case string:to_integer(Str) of
{Int, ""} -> Fun(Int);
_ -> false
end
end
end.
verify_claim(ClaimName, Required, JWT, Validator) ->
case JWT of
#{ClaimName := Value} ->
Validator(Value);
#{} ->
Required =:= optional
end.
do_authorize(Client, PubSub, Topic, AclRules) ->
do_authorize(Client, PubSub, Topic, AclRules, ?JWT_RULE_NAMES).
do_authorize(_Client, _PubSub, _Topic, _AclRules, []) ->
{matched, deny};
do_authorize(Client, PubSub, Topic, AclRules, [{Key, Action} | JWTRuleNames]) ->
TopicFilters = maps:get(Key, AclRules, []),
case
emqx_authz_rule:match(
Client,
PubSub,
Topic,
emqx_authz_rule:compile({allow, all, Action, TopicFilters})
)
of
{matched, Permission} -> {matched, Permission};
nomatch -> do_authorize(Client, PubSub, Topic, AclRules, JWTRuleNames)
end.

View File

@ -70,8 +70,7 @@ fields("authorization") ->
hoconsc:ref(?MODULE, postgresql), hoconsc:ref(?MODULE, postgresql),
hoconsc:ref(?MODULE, redis_single), hoconsc:ref(?MODULE, redis_single),
hoconsc:ref(?MODULE, redis_sentinel), hoconsc:ref(?MODULE, redis_sentinel),
hoconsc:ref(?MODULE, redis_cluster), hoconsc:ref(?MODULE, redis_cluster)
hoconsc:ref(?MODULE, jwt)
] ]
), ),
default => [], default => [],
@ -129,15 +128,6 @@ fields(redis_cluster) ->
authz_common_fields(redis) ++ authz_common_fields(redis) ++
connector_fields(redis, cluster) ++ connector_fields(redis, cluster) ++
[{cmd, cmd()}]; [{cmd, cmd()}];
fields(jwt) ->
authz_common_fields(jwt) ++
[
{acl_claim_name, #{
type => binary(),
default => <<"acl">>,
desc => ?DESC(acl_claim_name)
}}
];
fields("metrics_status_fields") -> fields("metrics_status_fields") ->
[ [
{"resource_metrics", mk(ref(?MODULE, "resource_metrics"), #{desc => ?DESC("metrics")})}, {"resource_metrics", mk(ref(?MODULE, "resource_metrics"), #{desc => ?DESC("metrics")})},
@ -236,8 +226,6 @@ desc(redis_sentinel) ->
?DESC(redis_sentinel); ?DESC(redis_sentinel);
desc(redis_cluster) -> desc(redis_cluster) ->
?DESC(redis_cluster); ?DESC(redis_cluster);
desc(jwt) ->
?DESC(jwt);
desc(_) -> desc(_) ->
undefined. undefined.

View File

@ -58,7 +58,6 @@ init_per_testcase(_TestCase, Config) ->
), ),
ok = emqx_authz_test_lib:reset_authorizers(), ok = emqx_authz_test_lib:reset_authorizers(),
{ok, _} = emqx_authz:update(replace, [authz_config()]),
Config. Config.
end_per_testcase(_TestCase, _Config) -> end_per_testcase(_TestCase, _Config) ->
@ -317,17 +316,12 @@ authn_config() ->
<<"algorithm">> => <<"hmac-based">>, <<"algorithm">> => <<"hmac-based">>,
<<"secret">> => ?SECRET, <<"secret">> => ?SECRET,
<<"secret_base64_encoded">> => <<"false">>, <<"secret_base64_encoded">> => <<"false">>,
<<"acl_claim_name">> => <<"acl">>,
<<"verify_claims">> => #{ <<"verify_claims">> => #{
<<"username">> => ?PH_USERNAME <<"username">> => ?PH_USERNAME
} }
}. }.
authz_config() ->
#{
<<"type">> => <<"jwt">>,
<<"acl_claim_name">> => <<"acl">>
}.
generate_jws(Payload) -> generate_jws(Payload) ->
JWK = jose_jwk:from_oct(?SECRET), JWK = jose_jwk:from_oct(?SECRET),
Header = #{ Header = #{