diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 58d0c7e56..3d610bafc 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1654,14 +1654,14 @@ do_authenticate( ) -> Properties = #{'Authentication-Method' => AuthMethod}, case emqx_access_control:authenticate(Credential) of - {ok, Result} -> + {ok, AuthResult} -> {ok, Properties, Channel#channel{ - clientinfo = ClientInfo#{is_superuser => maps:get(is_superuser, Result, false)}, + clientinfo = merge_auth_result(ClientInfo, AuthResult), auth_cache = #{} }}; - {ok, Result, AuthData} -> + {ok, AuthResult, AuthData} -> {ok, Properties#{'Authentication-Data' => AuthData}, Channel#channel{ - clientinfo = ClientInfo#{is_superuser => maps:get(is_superuser, Result, false)}, + clientinfo = merge_auth_result(ClientInfo, AuthResult), auth_cache = #{} }}; {continue, AuthCache} -> @@ -1675,12 +1675,16 @@ do_authenticate( end; do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) -> case emqx_access_control:authenticate(Credential) of - {ok, #{is_superuser := IsSuperuser}} -> - {ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => IsSuperuser}}}; + {ok, AuthResult} -> + {ok, #{}, Channel#channel{clientinfo = merge_auth_result(ClientInfo, AuthResult)}}; {error, Reason} -> {error, emqx_reason_codes:connack_error(Reason)} end. +merge_auth_result(ClientInfo, AuthResult) when is_map(ClientInfo) andalso is_map(AuthResult) -> + IsSuperuser = maps:get(is_superuser, AuthResult, false), + maps:merge(ClientInfo, AuthResult#{is_superuser => IsSuperuser}). + %%-------------------------------------------------------------------- %% Process Topic Alias diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index c5d84e98d..58c51facb 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -376,7 +376,7 @@ do_verify(JWS, [JWK | More], VerifyClaims) -> Claims = emqx_json:decode(Payload, [return_maps]), case verify_claims(Claims, VerifyClaims) of ok -> - {ok, emqx_authn_utils:is_superuser(Claims)}; + {ok, maps:put(jwt, Claims, emqx_authn_utils:is_superuser(Claims))}; {error, Reason} -> {error, Reason} end; @@ -393,13 +393,13 @@ verify_claims(Claims, VerifyClaims0) -> VerifyClaims = [ {<<"exp">>, fun(ExpireTime) -> - Now < ExpireTime + is_integer(ExpireTime) andalso Now < ExpireTime end}, {<<"iat">>, fun(IssueAt) -> - IssueAt =< Now + is_integer(IssueAt) andalso IssueAt =< Now end}, {<<"nbf">>, fun(NotBefore) -> - NotBefore =< Now + is_integer(NotBefore) andalso NotBefore =< Now end} ] ++ VerifyClaims0, do_verify_claims(Claims, VerifyClaims). diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index e8e5e73ae..f7534b880 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -70,7 +70,7 @@ t_jwt_authenticator_hmac_based(_) -> username => <<"myuser">>, password => JWS }, - ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State)), + ?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential, State)), Payload1 = #{<<"username">> => <<"myuser">>, <<"is_superuser">> => true}, JWS1 = generate_jws('hmac-based', Payload1, Secret), @@ -78,7 +78,7 @@ t_jwt_authenticator_hmac_based(_) -> username => <<"myuser">>, password => JWS1 }, - ?assertEqual({ok, #{is_superuser => true}}, emqx_authn_jwt:authenticate(Credential1, State)), + ?assertMatch({ok, #{is_superuser := true}}, emqx_authn_jwt:authenticate(Credential1, State)), BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), Credential2 = Credential#{password => BadJWS}, @@ -90,7 +90,7 @@ t_jwt_authenticator_hmac_based(_) -> secret_base64_encoded => true }, {ok, State2} = emqx_authn_jwt:update(Config2, State), - ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State2)), + ?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential, State2)), %% invalid secret BadConfig = Config#{ @@ -101,7 +101,7 @@ t_jwt_authenticator_hmac_based(_) -> Config3 = Config#{verify_claims => [{<<"username">>, <<"${username}">>}]}, {ok, State3} = emqx_authn_jwt:update(Config3, State2), - ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State3)), + ?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential, State3)), ?assertEqual( {error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential#{username => <<"otheruser">>}, State3) @@ -124,7 +124,7 @@ t_jwt_authenticator_hmac_based(_) -> }, JWS4 = generate_jws('hmac-based', Payload4, Secret), Credential4 = Credential#{password => JWS4}, - ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential4, State3)), + ?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential4, State3)), %% Issued At Payload5 = #{ @@ -133,7 +133,7 @@ t_jwt_authenticator_hmac_based(_) -> }, JWS5 = generate_jws('hmac-based', Payload5, Secret), Credential5 = Credential#{password => JWS5}, - ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential5, State3)), + ?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential5, State3)), Payload6 = #{ <<"username">> => <<"myuser">>, @@ -152,7 +152,7 @@ t_jwt_authenticator_hmac_based(_) -> }, JWS7 = generate_jws('hmac-based', Payload7, Secret), Credential7 = Credential6#{password => JWS7}, - ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential7, State3)), + ?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential7, State3)), Payload8 = #{ <<"username">> => <<"myuser">>, @@ -185,7 +185,7 @@ t_jwt_authenticator_public_key(_) -> username => <<"myuser">>, password => JWS }, - ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State)), + ?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential, State)), ?assertEqual( ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State) ), @@ -280,7 +280,7 @@ t_jwks_renewal(_Config) -> ok = snabbkaffe:stop(), - ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential1, State2)), + ?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential1, State2)), ?assertEqual( {error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential1#{password => JWS2}, State2) @@ -307,7 +307,7 @@ t_jwt_authenticator_verify_claims(_) -> username => <<"myuser">>, password => JWS0 }, - ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential0, State0)), + ?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential0, State0)), Config1 = Config0#{ verify_claims => [{<<"foo">>, <<"${username}">>}] @@ -340,7 +340,7 @@ t_jwt_authenticator_verify_claims(_) -> username => <<"myuser">>, password => JWS3 }, - ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential3, State1)). + ?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential3, State1)). %%------------------------------------------------------------------------------ %% Helpers diff --git a/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf b/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf index d0aa7a8ca..b08563267 100644 --- a/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf +++ b/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf @@ -340,6 +340,28 @@ Commands can support following wildcards:\n } } + jwt { + desc { + en: """Authorization using ACL rules from authentication JWT.""" + zh: """Authorization using ACL rules from authentication JWT.""" + } + 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 { desc { en: """Database query used to retrieve authorization data.""" diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 53083f9b3..953dad27c 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -384,6 +384,8 @@ type(postgresql) -> postgresql; type(<<"postgresql">>) -> postgresql; type(built_in_database) -> built_in_database; type(<<"built_in_database">>) -> built_in_database; +type(jwt) -> jwt; +type(<<"jwt">>) -> jwt; %% should never happen if the input is type-checked by hocon schema type(Unknown) -> throw({unknown_authz_source_type, Unknown}). diff --git a/apps/emqx_authz/src/emqx_authz_jwt.erl b/apps/emqx_authz/src/emqx_authz_jwt.erl new file mode 100644 index 000000000..5f8e2f45d --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_jwt.erl @@ -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_jwt). + +-include_lib("emqx/include/logger.hrl"). + +-behaviour(emqx_authz). + +-ifdef(TEST). +-compile(export_all). +-compile(nowarn_export_all). +-endif. + +%% APIs +-export([ + description/0, + init/1, + destroy/1, + authorize/4 +]). + +-define(JWT_RULE_NAMES, [ + {<<"pub">>, publish}, + {<<"sub">>, subscribe}, + {<<"all">>, all} +]). + +%%-------------------------------------------------------------------- +%% emqx_authz callbacks +%%-------------------------------------------------------------------- + +description() -> + "AuthZ with JWT". + +init(#{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">>, fun(ExpireTime) -> + is_integer(ExpireTime) andalso Now < ExpireTime + end}, + {<<"iat">>, fun(IssueAt) -> + is_integer(IssueAt) andalso IssueAt =< Now + end}, + {<<"nbf">>, fun(NotBefore) -> + is_integer(NotBefore) andalso NotBefore =< Now + end} + ], + IsValid = lists:all( + fun({ClaimName, Validator}) -> + (not maps:is_key(ClaimName, JWT)) orelse + Validator(maps:get(ClaimName, JWT)) + end, + VerifyClaims + ), + case IsValid of + true -> {ok, JWT}; + false -> error + 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. diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 042568ce4..f61bd258d 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -67,7 +67,8 @@ fields("authorization") -> hoconsc:ref(?MODULE, postgresql), hoconsc:ref(?MODULE, redis_single), hoconsc:ref(?MODULE, redis_sentinel), - hoconsc:ref(?MODULE, redis_cluster) + hoconsc:ref(?MODULE, redis_cluster), + hoconsc:ref(?MODULE, jwt) ] ), default => [], @@ -124,7 +125,16 @@ fields(redis_sentinel) -> fields(redis_cluster) -> authz_common_fields(redis) ++ 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) + }} + ]. desc(?CONF_NS) -> ?DESC(?CONF_NS); @@ -152,6 +162,8 @@ desc(redis_sentinel) -> ?DESC(redis_sentinel); desc(redis_cluster) -> ?DESC(redis_cluster); +desc(jwt) -> + ?DESC(jwt); desc(_) -> undefined. diff --git a/apps/emqx_authz/test/emqx_authz_jwt_SUITE.erl b/apps/emqx_authz/test/emqx_authz_jwt_SUITE.erl new file mode 100644 index 000000000..30e7aa4e7 --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_jwt_SUITE.erl @@ -0,0 +1,312 @@ +%%-------------------------------------------------------------------- +%% 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_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include_lib("emqx/include/emqx_placeholder.hrl"). +-include_lib("emqx_authn/include/emqx_authn.hrl"). +-include_lib("emqx/include/emqx_mqtt.hrl"). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(SECRET, <<"some_secret">>). +-define(AUTHN_PATH, [authentication]). + +all() -> + emqx_common_test_helpers:all(?MODULE). + +groups() -> + []. + +init_per_suite(Config) -> + ok = emqx_common_test_helpers:start_apps( + [emqx_conf, emqx_authn, emqx_authz], + fun set_special_configs/1 + ), + ok = emqx_authentication:initialize_authentication(?GLOBAL, []), + Config. + +end_per_suite(_Config) -> + ok = emqx_common_test_helpers:stop_apps([emqx_authn, emqx_authz, emqx_conf]). + +init_per_testcase(_TestCase, Config) -> + emqx_authn_test_lib:delete_authenticators( + ?AUTHN_PATH, + ?GLOBAL + ), + AuthConfig = authn_config(), + {ok, _} = emqx:update_config( + ?AUTHN_PATH, + {create_authenticator, ?GLOBAL, AuthConfig} + ), + + ok = emqx_authz_test_lib:reset_authorizers(), + {ok, _} = emqx_authz:update(replace, [authz_config()]), + Config. + +end_per_testcase(_TestCase, _Config) -> + emqx_authn_test_lib:delete_authenticators( + ?AUTHN_PATH, + ?GLOBAL + ), + ok = emqx_authz_test_lib:restore_authorizers(). + +set_special_configs(emqx_authz) -> + ok = emqx_authz_test_lib:reset_authorizers(); +set_special_configs(_) -> + ok. + +%%------------------------------------------------------------------------------ +%% Tests +%%------------------------------------------------------------------------------ + +t_topic_rules(_Config) -> + Payload = #{ + <<"exp">> => erlang:system_time(second) + 60, + <<"acl">> => #{ + <<"pub">> => [ + <<"eq testpub1/${username}">>, + <<"testpub2/${clientid}">>, + <<"testpub3/#">> + ], + <<"sub">> => [ + <<"eq testsub1/${username}">>, + <<"testsub2/${clientid}">>, + <<"testsub3/#">> + ], + <<"all">> => [ + <<"eq testall1/${username}">>, + <<"testall2/${clientid}">>, + <<"testall3/#">> + ] + }, + <<"username">> => <<"username">> + }, + JWT = generate_jws(Payload), + + {ok, C} = emqtt:start_link( + [ + {clean_start, true}, + {proto_ver, v5}, + {clientid, <<"clientid">>}, + {username, <<"username">>}, + {password, JWT} + ] + ), + {ok, _} = emqtt:connect(C), + + Cases = [ + {deny, <<"testpub1/username">>}, + {deny, <<"testpub2/clientid">>}, + {deny, <<"testpub3/foobar">>}, + + {deny, <<"testsub1/username">>}, + {allow, <<"testsub1/${username}">>}, + {allow, <<"testsub2/clientid">>}, + {allow, <<"testsub3/foobar">>}, + {allow, <<"testsub3/+/foobar">>}, + {allow, <<"testsub3/#">>}, + + {deny, <<"testsub2/username">>}, + {deny, <<"testsub1/clientid">>}, + {deny, <<"testsub4/foobar">>}, + + {deny, <<"testall1/username">>}, + {allow, <<"testall1/${username}">>}, + {allow, <<"testall2/clientid">>}, + {allow, <<"testall3/foobar">>}, + {allow, <<"testall3/+/foobar">>}, + {allow, <<"testall3/#">>}, + + {deny, <<"testall2/username">>}, + {deny, <<"testall1/clientid">>}, + {deny, <<"testall4/foobar">>} + ], + + lists:foreach( + fun + ({allow, Topic}) -> + ?assertMatch( + {ok, #{}, [0]}, + emqtt:subscribe(C, Topic, 0) + ); + ({deny, Topic}) -> + ?assertMatch( + {ok, #{}, [?RC_NOT_AUTHORIZED]}, + emqtt:subscribe(C, Topic, 0) + ) + end, + Cases + ), + + ok = emqtt:disconnect(C). + +t_check_pub(_Config) -> + Payload = #{ + <<"username">> => <<"username">>, + <<"acl">> => #{<<"sub">> => [<<"a/b">>]}, + <<"exp">> => erlang:system_time(second) + 10 + }, + + JWT = generate_jws(Payload), + + {ok, C} = emqtt:start_link( + [ + {clean_start, true}, + {proto_ver, v5}, + {clientid, <<"clientid">>}, + {username, <<"username">>}, + {password, JWT} + ] + ), + {ok, _} = emqtt:connect(C), + + ok = emqtt:publish(C, <<"a/b">>, <<"hi">>, 0), + + receive + {publish, #{topic := <<"a/b">>}} -> + ?assert(false, "Publish to `a/b` should not be allowed") + after 100 -> ok + end, + + ok = emqtt:disconnect(C). + +t_check_no_recs(_Config) -> + Payload = #{ + <<"username">> => <<"username">>, + <<"acl">> => #{}, + <<"exp">> => erlang:system_time(second) + 10 + }, + + JWT = generate_jws(Payload), + + {ok, C} = emqtt:start_link( + [ + {clean_start, true}, + {proto_ver, v5}, + {clientid, <<"clientid">>}, + {username, <<"username">>}, + {password, JWT} + ] + ), + {ok, _} = emqtt:connect(C), + + ?assertMatch( + {ok, #{}, [?RC_NOT_AUTHORIZED]}, + emqtt:subscribe(C, <<"a/b">>, 0) + ), + + ok = emqtt:disconnect(C). + +t_check_no_acl_claim(_Config) -> + Payload = #{ + <<"username">> => <<"username">>, + <<"exp">> => erlang:system_time(second) + 10 + }, + + JWT = generate_jws(Payload), + + {ok, C} = emqtt:start_link( + [ + {clean_start, true}, + {proto_ver, v5}, + {clientid, <<"clientid">>}, + {username, <<"username">>}, + {password, JWT} + ] + ), + {ok, _} = emqtt:connect(C), + + ?assertMatch( + {ok, #{}, [?RC_NOT_AUTHORIZED]}, + emqtt:subscribe(C, <<"a/b">>, 0) + ), + + ok = emqtt:disconnect(C). + +t_check_expire(_Config) -> + Payload = #{ + <<"username">> => <<"username">>, + <<"acl">> => #{<<"sub">> => [<<"a/b">>]}, + <<"exp">> => erlang:system_time(second) + 1 + }, + + JWT = generate_jws(Payload), + + {ok, C} = emqtt:start_link( + [ + {clean_start, true}, + {proto_ver, v5}, + {clientid, <<"clientid">>}, + {username, <<"username">>}, + {password, JWT} + ] + ), + {ok, _} = emqtt:connect(C), + ?assertMatch( + {ok, #{}, [0]}, + emqtt:subscribe(C, <<"a/b">>, 0) + ), + + ?assertMatch( + {ok, #{}, [0]}, + emqtt:unsubscribe(C, <<"a/b">>) + ), + + timer:sleep(2000), + + ?assertMatch( + {ok, #{}, [?RC_NOT_AUTHORIZED]}, + emqtt:subscribe(C, <<"a/b">>, 0) + ), + + ok = emqtt:disconnect(C). + +%%------------------------------------------------------------------------------ +%% Helpers +%%------------------------------------------------------------------------------ + +authn_config() -> + #{ + <<"mechanism">> => <<"jwt">>, + <<"use_jwks">> => <<"false">>, + <<"algorithm">> => <<"hmac-based">>, + <<"secret">> => ?SECRET, + <<"secret_base64_encoded">> => <<"false">>, + <<"verify_claims">> => #{ + <<"username">> => ?PH_USERNAME + } + }. + +authz_config() -> + #{ + <<"type">> => <<"jwt">>, + <<"acl_claim_name">> => <<"acl">> + }. + +generate_jws(Payload) -> + JWK = jose_jwk:from_oct(?SECRET), + Header = #{ + <<"alg">> => <<"HS256">>, + <<"typ">> => <<"JWT">> + }, + Signed = jose_jwt:sign(JWK, Header, Payload), + {_, JWS} = jose_jws:compact(Signed), + JWS.