Fold uaa_jwt into this plugin
Per discussion with @hairyhum.
This commit is contained in:
parent
f582760664
commit
5b002c5eab
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
@ -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">>
|
||||
}).
|
||||
|
|
@ -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.
|
||||
Loading…
Reference in New Issue