From d0f686d19db9619aacc2b2ccdb1f3492027d14bb Mon Sep 17 00:00:00 2001 From: Ilya Averyanov Date: Tue, 14 Jun 2022 12:43:48 +0300 Subject: [PATCH] feat(jwt authn): allow to specify JWT field --- apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf | 17 +++++++- .../src/simple_authn/emqx_authn_jwt.erl | 40 ++++++++++++++----- apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl | 32 +++++++++++++-- 3 files changed, 73 insertions(+), 16 deletions(-) diff --git a/apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf b/apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf index 69fb628a1..80d389de4 100644 --- a/apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf +++ b/apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf @@ -217,8 +217,21 @@ Authentication will verify that the value of claims in the JWT (taken from the P zh: """JWT claim name to use for getting ACL rules.""" } label { - en: """acl_claim_name""" - zh: """acl_claim_name""" + en: """ACL claim name""" + zh: """ACL claim name""" } } + + from { + desc { + en: """Field to take JWT from.""" + zh: """要从中获取 JWT 的字段。""" + } + label { + en: """From Field""" + zh: """源字段""" + } + } + + } 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 3c34307fc..6b62cec34 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -117,7 +117,8 @@ common_fields() -> default => <<"acl">>, desc => ?DESC(acl_claim_name) }}, - {verify_claims, fun verify_claims/1} + {verify_claims, fun verify_claims/1}, + {from, fun from/1} ] ++ emqx_authn_schema:common_fields(). secret(type) -> binary(); @@ -184,6 +185,11 @@ verify_claims(required) -> verify_claims(_) -> undefined. +from(type) -> hoconsc:enum([username, password]); +from(desc) -> ?DESC(?FUNCTION_NAME); +from(default) -> password; +from(_) -> undefined. + %%------------------------------------------------------------------------------ %% APIs %%------------------------------------------------------------------------------ @@ -234,22 +240,25 @@ update(#{use_jwks := true} = Config, _State) -> authenticate(#{auth_method := _}, _) -> ignore; authenticate( - Credential = #{password := JWT}, + Credential, #{ verify_claims := VerifyClaims0, jwk := JWK, - acl_claim_name := AclClaimName + acl_claim_name := AclClaimName, + from := From } ) -> + JWT = maps:get(From, Credential), JWKs = [JWK], VerifyClaims = replace_placeholder(VerifyClaims0, Credential), verify(JWT, JWKs, VerifyClaims, AclClaimName); authenticate( - Credential = #{password := JWT}, + Credential, #{ verify_claims := VerifyClaims0, jwk_resource := ResourceId, - acl_claim_name := AclClaimName + acl_claim_name := AclClaimName, + from := From } ) -> case emqx_resource:query(ResourceId, get_jwks) of @@ -261,6 +270,7 @@ authenticate( }), ignore; {ok, JWKs} -> + JWT = maps:get(From, Credential), VerifyClaims = replace_placeholder(VerifyClaims0, Credential), verify(JWT, JWKs, VerifyClaims, AclClaimName) end. @@ -281,7 +291,8 @@ create2(#{ secret := Secret0, secret_base64_encoded := Base64Encoded, verify_claims := VerifyClaims, - acl_claim_name := AclClaimName + acl_claim_name := AclClaimName, + from := From }) -> case may_decode_secret(Base64Encoded, Secret0) of {error, Reason} -> @@ -291,7 +302,8 @@ create2(#{ {ok, #{ jwk => JWK, verify_claims => VerifyClaims, - acl_claim_name => AclClaimName + acl_claim_name => AclClaimName, + from => From }} end; create2(#{ @@ -299,19 +311,22 @@ create2(#{ algorithm := 'public-key', public_key := PublicKey, verify_claims := VerifyClaims, - acl_claim_name := AclClaimName + acl_claim_name := AclClaimName, + from := From }) -> JWK = create_jwk_from_public_key(PublicKey), {ok, #{ jwk => JWK, verify_claims => VerifyClaims, - acl_claim_name => AclClaimName + acl_claim_name => AclClaimName, + from => From }}; create2( #{ use_jwks := true, verify_claims := VerifyClaims, - acl_claim_name := AclClaimName + acl_claim_name := AclClaimName, + from := From } = Config ) -> ResourceId = emqx_authn_utils:make_resource_id(?MODULE), @@ -324,7 +339,8 @@ create2( {ok, #{ jwk_resource => ResourceId, verify_claims => VerifyClaims, - acl_claim_name => AclClaimName + acl_claim_name => AclClaimName, + from => From }}. create_jwk_from_public_key(PublicKey) when @@ -366,6 +382,8 @@ replace_placeholder([{Name, {placeholder, PL}} | More], Variables, Acc) -> replace_placeholder([{Name, Value} | More], Variables, Acc) -> replace_placeholder(More, Variables, [{Name, Value} | Acc]). +verify(undefined, _, _, _) -> + ignore; verify(JWT, JWKs, VerifyClaims, AclClaimName) -> case do_verify(JWT, JWKs, VerifyClaims) of {ok, Extra} -> {ok, acl(Extra, AclClaimName)}; diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index d489c56a3..69ed42cd7 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -52,10 +52,11 @@ end_per_suite(_) -> %% Tests %%------------------------------------------------------------------------------ -t_jwt_authenticator_hmac_based(_) -> +t_hmac_based(_) -> Secret = <<"abcdef">>, Config = #{ mechanism => jwt, + from => password, acl_claim_name => <<"acl">>, use_jwks => false, algorithm => 'hmac-based', @@ -175,11 +176,12 @@ t_jwt_authenticator_hmac_based(_) -> ?assertEqual(ok, emqx_authn_jwt:destroy(State3)), ok. -t_jwt_authenticator_public_key(_) -> +t_public_key(_) -> PublicKey = test_rsa_key(public), PrivateKey = test_rsa_key(private), Config = #{ mechanism => jwt, + from => password, acl_claim_name => <<"acl">>, use_jwks => false, algorithm => 'public-key', @@ -202,6 +204,28 @@ t_jwt_authenticator_public_key(_) -> ?assertEqual(ok, emqx_authn_jwt:destroy(State)), ok. +t_jwt_in_username(_) -> + Secret = <<"abcdef">>, + Config = #{ + mechanism => jwt, + from => username, + acl_claim_name => <<"acl">>, + use_jwks => false, + algorithm => 'hmac-based', + secret => Secret, + secret_base64_encoded => false, + verify_claims => [] + }, + {ok, State} = emqx_authn_jwt:create(?AUTHN_ID, Config), + + Payload = #{<<"exp">> => erlang:system_time(second) + 60}, + JWS = generate_jws('hmac-based', Payload, Secret), + Credential = #{ + username => JWS, + password => <<"pass">> + }, + ?assertMatch({ok, #{is_superuser := false}}, emqx_authn_jwt:authenticate(Credential, State)). + t_jwks_renewal(_Config) -> {ok, _} = emqx_authn_http_test_server:start_link(?JWKS_PORT, ?JWKS_PATH, server_ssl_opts()), ok = emqx_authn_http_test_server:set_handler(fun jwks_handler/2), @@ -216,6 +240,7 @@ t_jwks_renewal(_Config) -> BadConfig0 = #{ mechanism => jwt, + from => password, acl_claim_name => <<"acl">>, algorithm => 'public-key', ssl => #{enable => false}, @@ -307,10 +332,11 @@ t_jwks_renewal(_Config) -> ?assertEqual(ok, emqx_authn_jwt:destroy(State2)), ok = emqx_authn_http_test_server:stop(). -t_jwt_authenticator_verify_claims(_) -> +t_verify_claims(_) -> Secret = <<"abcdef">>, Config0 = #{ mechanism => jwt, + from => password, acl_claim_name => <<"acl">>, use_jwks => false, algorithm => 'hmac-based',