Fold uaa_jwt into this plugin

Per discussion with @hairyhum.
This commit is contained in:
Michael Klishin 2018-07-19 19:22:47 +03:00
parent f582760664
commit 5b002c5eab
4 changed files with 220 additions and 2 deletions

View File

@ -2,12 +2,11 @@ PROJECT = rabbitmq_auth_backend_uaa
PROJECT_DESCRIPTION = OAuth 2 and JWT-based AuthN and AuthZ backend
BUILD_DEPS = rabbit_common
DEPS = uaa_jwt rabbit cowlib
DEPS = rabbit cowlib jose
TEST_DEPS = cowboy rabbitmq_web_dispatch rabbitmq_ct_helpers
DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk
dep_uaa_jwt = git_rmq uaa_jwt $(current_rmq_ref) $(base_rmq_ref) master
dep_jose = hex 1.8.4
# FIXME: Use erlang.mk patched for RabbitMQ, while waiting for PRs to be

View File

@ -0,0 +1,99 @@
-module(uaa_jwt).
-export([add_signing_key/3,
remove_signing_key/1,
decode_and_verify/1,
get_jwk/1,
verify_signing_key/2,
signing_keys/0]).
-export([client_id/1, sub/1, client_id/2, sub/2]).
-include_lib("jose/include/jose_jwk.hrl").
-type key_type() :: json | pem | map.
-spec add_signing_key(binary(), key_type(), binary() | map()) -> {ok, map()} | {error, term()}.
add_signing_key(KeyId, Type, Value) ->
case verify_signing_key(Type, Value) of
ok ->
NewSigningKeys = maps:put(KeyId, {Type, Value}, signing_keys()),
{ok, application:set_env(uaa_jwt, signing_keys, NewSigningKeys)};
{error, _} = Err ->
Err
end.
remove_signing_key(KeyId) ->
Keys = application:get_env(uaa_jwt, signing_keys, #{}),
application:set_env(uaa_jwt, signing_keys, maps:remove(KeyId, Keys)).
-spec decode_and_verify(binary()) -> {boolean(), map()} | {error, term()}.
decode_and_verify(Token) ->
case uaa_jwt_jwt:get_key_id(Token) of
{ok, KeyId} ->
case get_jwk(KeyId) of
{ok, JWK} ->
uaa_jwt_jwt:decode_and_verify(JWK, Token);
{error, _} = Err ->
Err
end;
{error, _} = Err ->
Err
end.
-spec get_jwk(binary()) -> {ok, map()} | {error, term()}.
get_jwk(KeyId) ->
Keys = signing_keys(),
case maps:get(KeyId, Keys, undefined) of
undefined ->
{error, key_not_found};
{Type, Value} ->
case Type of
json -> uaa_jwt_jwk:make_jwk(Value);
pem -> uaa_jwt_jwk:from_pem(Value);
pem_file -> uaa_jwt_jwk:from_pem_file(Value);
map -> uaa_jwt_jwk:make_jwk(Value);
_ -> {error, unknown_signing_key_type}
end
end.
verify_signing_key(Type, Value) ->
Verified = case Type of
json -> uaa_jwt_jwk:make_jwk(Value);
pem -> uaa_jwt_jwk:from_pem(Value);
pem_file -> uaa_jwt_jwk:from_pem_file(Value);
map -> uaa_jwt_jwk:make_jwk(Value);
_ -> {error, unknown_signing_key_type}
end,
case Verified of
{ok, Key} ->
case jose_jwk:from(Key) of
#jose_jwk{} -> ok;
{error, Reason} -> {error, Reason}
end;
Err -> Err
end.
signing_keys() ->
application:get_env(uaa_jwt, signing_keys, #{}).
-spec client_id(map()) -> binary() | undefined.
client_id(DecodedToken) ->
maps:get(<<"client_id">>, DecodedToken, undefined).
-spec client_id(map(), any()) -> binary() | undefined.
client_id(DecodedToken, Default) ->
maps:get(<<"client_id">>, DecodedToken, Default).
-spec sub(map()) -> binary() | undefined.
sub(DecodedToken) ->
maps:get(<<"sub">>, DecodedToken, undefined).
-spec sub(map(), any()) -> binary() | undefined.
sub(DecodedToken, Default) ->
maps:get(<<"sub">>, DecodedToken, Default).

View File

@ -0,0 +1,79 @@
-module(uaa_jwt_jwk).
-export([make_jwk/1, from_pem/1, from_pem_file/1]).
-include_lib("jose/include/jose_jwk.hrl").
-spec make_jwk(binary() | map()) -> {ok, #{binary() => binary()}} | {error, term()}.
make_jwk(Json) when is_binary(Json); is_list(Json) ->
JsonMap = jose:decode(iolist_to_binary(Json)),
make_jwk(JsonMap);
make_jwk(JsonMap) when is_map(JsonMap) ->
case JsonMap of
#{<<"kty">> := <<"MAC">>, <<"value">> := _Value} ->
{ok, mac_to_oct(JsonMap)};
#{<<"kty">> := <<"RSA">>, <<"n">> := _N, <<"e">> := _E} ->
{ok, fix_alg(JsonMap)};
#{<<"kty">> := <<"oct">>, <<"k">> := _K} ->
{ok, fix_alg(JsonMap)};
#{<<"kty">> := <<"OKP">>, <<"crv">> := _Crv, <<"x">> := _X} ->
{ok, fix_alg(JsonMap)};
#{<<"kty">> := <<"EC">>} ->
{ok, fix_alg(JsonMap)};
#{<<"kty">> := Kty} when Kty == <<"oct">>;
Kty == <<"MAC">>;
Kty == <<"RSA">>;
Kty == <<"OKP">>;
Kty == <<"EC">> ->
{error, {fields_missing_for_kty, Kty}};
#{<<"kty">> := _Kty} ->
{error, unknown_kty};
#{} ->
{error, no_kty}
end.
from_pem(Pem) ->
case jose_jwk:from_pem(Pem) of
#jose_jwk{} = Jwk -> {ok, Jwk};
Other ->
error_logger:warning_msg("Error parsing jwk from pem: ", [Other]),
{error, invalid_pem_string}
end.
from_pem_file(FileName) ->
case filelib:is_file(FileName) of
false ->
{error, enoent};
true ->
case jose_jwk:from_pem_file(FileName) of
#jose_jwk{} = Jwk -> {ok, Jwk};
Other ->
error_logger:warning_msg("Error parsing jwk from pem file: ", [Other]),
{error, invalid_pem_file}
end
end.
mac_to_oct(#{<<"kty">> := <<"MAC">>, <<"value">> := Value} = Key) ->
OktKey = maps:merge(Key,
#{<<"kty">> => <<"oct">>,
<<"k">> => base64url:encode(Value)}),
fix_alg(OktKey).
fix_alg(#{<<"alg">> := Alg} = Key) ->
Algs = uaa_algs(),
case maps:get(Alg, Algs, undefined) of
undefined -> Key;
Val -> Key#{<<"alg">> := Val}
end;
fix_alg(#{} = Key) -> Key.
uaa_algs() ->
application:get_env(uaa_jwt_decoder, uaa_algs,
#{
<<"HMACSHA256">> => <<"HS256">>,
<<"HMACSHA384">> => <<"HS384">>,
<<"HMACSHA512">> => <<"HS512">>,
<<"SHA256withRSA">> => <<"RS256">>,
<<"SHA512withRSA">> => <<"RS512">>
}).

View File

@ -0,0 +1,41 @@
-module(uaa_jwt_jwt).
%% Transitional step until we can require Erlang/OTP 21 and
%% use the now recommended try/catch syntax for obtaining the stack trace.
-compile(nowarn_deprecated_function).
-export([decode/1, decode_and_verify/2, get_key_id/1]).
-include_lib("jose/include/jose_jwt.hrl").
-include_lib("jose/include/jose_jws.hrl").
decode(Token) ->
try
#jose_jwt{fields = Fields} = jose_jwt:peek_payload(Token),
Fields
catch Type:Err ->
{error, {invalid_token, Type, Err, erlang:get_stacktrace()}}
end.
decode_and_verify(Jwk, Token) ->
case jose_jwt:verify(Jwk, Token) of
{true, #jose_jwt{fields = Fields}, _} -> {true, Fields};
{false, #jose_jwt{fields = Fields}, _} -> {false, Fields}
end.
get_key_id(Token) ->
try
case jose_jwt:peek_protected(Token) of
#jose_jws{fields = #{<<"kid">> := Kid}} -> {ok, Kid};
#jose_jws{} -> get_default_key()
end
catch Type:Err ->
{error, {invalid_token, Type, Err, erlang:get_stacktrace()}}
end.
get_default_key() ->
case application:get_env(uaa_jwt, default_key, undefined) of
undefined -> {error, no_key};
Val -> {ok, Val}
end.