diff --git a/.github/workflows/test-authnz.yaml b/.github/workflows/test-authnz.yaml index 17ce8225cb9b..9347416fa698 100644 --- a/.github/workflows/test-authnz.yaml +++ b/.github/workflows/test-authnz.yaml @@ -74,20 +74,22 @@ jobs: - name: Run Suites id: tests run: | - IMAGE_TAG=$(find PACKAGES/rabbitmq-server-generic-unix-*.tar.xz | awk -F 'PACKAGES/rabbitmq-server-generic-unix-|.tar.xz' '{print $2}') - CONF_DIR_PREFIX="$(mktemp -d)" RABBITMQ_DOCKER_IMAGE=pivotalrabbitmq/rabbitmq:$IMAGE_TAG \ - ${SELENIUM_DIR}/run-suites.sh full-suite-authnz-messaging - echo "SELENIUM_ARTIFACTS=$CONF_DIR_PREFIX" >> "$GITHUB_OUTPUT" - + export IMAGE_TAG=$(find PACKAGES/rabbitmq-server-generic-unix-*.tar.xz | awk -F 'PACKAGES/rabbitmq-server-generic-unix-|.tar.xz' '{print $2}') + export CONF_DIR_PREFIX="$(mktemp -d)" + export RABBITMQ_DOCKER_IMAGE=pivotalrabbitmq/rabbitmq:$IMAGE_TAG + echo "Running selenium tests with " + echo " - CONF_DIR_PREFIX: ${CONF_DIR_PREFIX}" + echo " - IMAGE_TAG: ${IMAGE_TAG}" + echo " - RABBITMQ_DOCKER_IMAGE: ${RABBITMQ_DOCKER_IMAGE}" + echo "SELENIUM_ARTIFACTS=${CONF_DIR_PREFIX}" >> $GITHUB_ENV + ${SELENIUM_DIR}/run-suites.sh full-suite-authnz-messaging + - name: Upload Test Artifacts - if: always() + if: ${{ failure() && steps.tests.outcome == 'failure' }} uses: actions/upload-artifact@v4.3.2 - env: - SELENIUM_ARTIFACTS: ${{ steps.tests.outputs.SELENIUM_ARTIFACTS }} with: name: test-artifacts-${{ matrix.browser }}-${{ matrix.erlang_version }} - path: | - $SELENIUM_ARTIFACTS/* + path: ${{ env.SELENIUM_ARTIFACTS }}/* summary-selenium: needs: diff --git a/.github/workflows/test-management-ui-for-pr.yaml b/.github/workflows/test-management-ui-for-pr.yaml index 6b82138cca10..90400c4b7b35 100644 --- a/.github/workflows/test-management-ui-for-pr.yaml +++ b/.github/workflows/test-management-ui-for-pr.yaml @@ -62,17 +62,19 @@ jobs: - name: Run short UI suites on a standalone rabbitmq server id: tests run: | - IMAGE_TAG=$(find PACKAGES/rabbitmq-server-generic-unix-*.tar.xz | awk -F 'PACKAGES/rabbitmq-server-generic-unix-|.tar.xz' '{print $2}') - CONF_DIR_PREFIX="$(mktemp -d)" RABBITMQ_DOCKER_IMAGE=pivotalrabbitmq/rabbitmq:$IMAGE_TAG \ - ${SELENIUM_DIR}/run-suites.sh short-suite-management-ui - echo "SELENIUM_ARTIFACTS=$CONF_DIR_PREFIX" >> "$GITHUB_OUTPUT" - + export IMAGE_TAG=$(find PACKAGES/rabbitmq-server-generic-unix-*.tar.xz | awk -F 'PACKAGES/rabbitmq-server-generic-unix-|.tar.xz' '{print $2}') + export CONF_DIR_PREFIX="$(mktemp -d)" + export RABBITMQ_DOCKER_IMAGE=pivotalrabbitmq/rabbitmq:$IMAGE_TAG + echo "Running selenium tests with " + echo " - CONF_DIR_PREFIX: ${CONF_DIR_PREFIX}" + echo " - IMAGE_TAG: ${IMAGE_TAG}" + echo " - RABBITMQ_DOCKER_IMAGE: ${RABBITMQ_DOCKER_IMAGE}" + echo "SELENIUM_ARTIFACTS=${CONF_DIR_PREFIX}" >> $GITHUB_ENV + ${SELENIUM_DIR}/run-suites.sh short-suite-management-ui + - name: Upload Test Artifacts - if: ${{ failure() && steps.tests.outcome == 'failed' }} + if: ${{ failure() && steps.tests.outcome == 'failure' }} uses: actions/upload-artifact@v4 - env: - SELENIUM_ARTIFACTS: ${{ steps.tests.outputs.SELENIUM_ARTIFACTS }} with: name: test-artifacts-${{ matrix.browser }}-${{ matrix.erlang_version }} - path: | - $SELENIUM_ARTIFACTS/* + path: ${{ env.SELENIUM_ARTIFACTS }}/* diff --git a/.github/workflows/test-management-ui.yaml b/.github/workflows/test-management-ui.yaml index d240f327daf2..1768c0cbf5c2 100644 --- a/.github/workflows/test-management-ui.yaml +++ b/.github/workflows/test-management-ui.yaml @@ -66,17 +66,19 @@ jobs: - name: Run full UI suite on a 3-node rabbitmq cluster id: tests run: | - IMAGE_TAG=$(find PACKAGES/rabbitmq-server-generic-unix-*.tar.xz | awk -F 'PACKAGES/rabbitmq-server-generic-unix-|.tar.xz' '{print $2}') - CONF_DIR_PREFIX="$(mktemp -d)" RABBITMQ_DOCKER_IMAGE=pivotalrabbitmq/rabbitmq:$IMAGE_TAG \ - ${SELENIUM_DIR}/run-suites.sh full-suite-management-ui - echo "SELENIUM_ARTIFACTS=$CONF_DIR_PREFIX" >> "$GITHUB_OUTPUT" + export IMAGE_TAG=$(find PACKAGES/rabbitmq-server-generic-unix-*.tar.xz | awk -F 'PACKAGES/rabbitmq-server-generic-unix-|.tar.xz' '{print $2}') + export CONF_DIR_PREFIX="$(mktemp -d)" + export RABBITMQ_DOCKER_IMAGE=pivotalrabbitmq/rabbitmq:$IMAGE_TAG + echo "Running selenium tests with " + echo " - CONF_DIR_PREFIX: ${CONF_DIR_PREFIX}" + echo " - IMAGE_TAG: ${IMAGE_TAG}" + echo " - RABBITMQ_DOCKER_IMAGE: ${RABBITMQ_DOCKER_IMAGE}" + echo "SELENIUM_ARTIFACTS=${CONF_DIR_PREFIX}" >> $GITHUB_ENV + ${SELENIUM_DIR}/run-suites.sh full-suite-management-ui - name: Upload Test Artifacts - if: ${{ failure() && steps.tests.outcome == 'failed' }} + if: ${{ failure() && steps.tests.outcome == 'failure' }} uses: actions/upload-artifact@v4.3.2 - env: - SELENIUM_ARTIFACTS: ${{ steps.run-suites.outputs.SELENIUM_ARTIFACTS }} with: name: test-artifacts-${{ matrix.browser }}-${{ matrix.erlang_version }} - path: | - $SELENIUM_ARTIFACTS/* + path: ${{ env.SELENIUM_ARTIFACTS }}/* diff --git a/deps/oauth2_client/include/oauth2_client.hrl b/deps/oauth2_client/include/oauth2_client.hrl index 24534dc136f4..38daa88023a4 100644 --- a/deps/oauth2_client/include/oauth2_client.hrl +++ b/deps/oauth2_client/include/oauth2_client.hrl @@ -27,6 +27,7 @@ -define(REQUEST_CLIENT_SECRET, "client_secret"). -define(REQUEST_SCOPE, "scope"). -define(REQUEST_REFRESH_TOKEN, "refresh_token"). +-define(REQUEST_TOKEN, "token"). % define access token response constants -define(BEARER_TOKEN_TYPE, <<"Bearer">>). @@ -43,5 +44,6 @@ -define(RESPONSE_TOKEN_ENDPOINT, <<"token_endpoint">>). -define(RESPONSE_AUTHORIZATION_ENDPOINT, <<"authorization_endpoint">>). -define(RESPONSE_END_SESSION_ENDPOINT, <<"end_session_endpoint">>). +-define(RESPONSE_INTROSPECTION_ENDPOINT, <<"introspection_endpoint">>). -define(RESPONSE_JWKS_URI, <<"jwks_uri">>). -define(RESPONSE_TLS_OPTIONS, <<"ssl_options">>). diff --git a/deps/oauth2_client/include/types.hrl b/deps/oauth2_client/include/types.hrl index 622cae22202c..4ae82f21a5b7 100644 --- a/deps/oauth2_client/include/types.hrl +++ b/deps/oauth2_client/include/types.hrl @@ -16,7 +16,8 @@ token_endpoint :: option(uri_string:uri_string()), authorization_endpoint :: option(uri_string:uri_string()), end_session_endpoint :: option(uri_string:uri_string()), - jwks_uri :: option(uri_string:uri_string()) + jwks_uri :: option(uri_string:uri_string()), + introspection_endpoint :: option(uri_string:uri_string()) }). -type openid_configuration() :: #openid_configuration{}. @@ -28,6 +29,10 @@ authorization_endpoint :: option(uri_string:uri_string()), end_session_endpoint :: option(uri_string:uri_string()), jwks_uri :: option(uri_string:uri_string()), + introspection_endpoint :: option(uri_string:uri_string()), + introspection_client_id :: binary() | undefined, + introspection_client_secret :: binary() | undefined, + introspection_client_auth_method :: basic | request_param | undefined, ssl_options :: option(list()) }). @@ -73,3 +78,27 @@ }). -type refresh_token_request() :: #refresh_token_request{}. + +-record(introspect_token_request, { + endpoint :: option(uri_string:uri_string()), + client_id :: binary() | undefined, + client_secret :: binary() | undefined, + client_auth_method :: basic | request_param | undefined, + ssl_options :: option(list()) +}). + +-type introspect_token_request() :: #introspect_token_request{}. + +-record(unsuccessful_introspect_token_response, { + error :: binary() | string() | number(), + error_description :: binary() | string() | undefined +}). + +-type unsuccessful_introspect_token_response() :: #unsuccessful_introspect_token_response{}. + +-record(signing_key, { + id :: string(), + type :: hs256 | rs256, + key :: option(#{binary() => binary()}) +}). +-type signing_key() :: #signing_key{}. diff --git a/deps/oauth2_client/src/oauth2_client.erl b/deps/oauth2_client/src/oauth2_client.erl index 1aba46033d22..d6024c8de446 100644 --- a/deps/oauth2_client/src/oauth2_client.erl +++ b/deps/oauth2_client/src/oauth2_client.erl @@ -7,17 +7,21 @@ -module(oauth2_client). -export([get_access_token/2, get_expiration_time/1, refresh_access_token/2, + introspect_token/1,sign_token/1, + get_opaque_token_signing_key/0,get_opaque_token_signing_key/1, get_oauth_provider/1, get_oauth_provider/2, get_openid_configuration/2, build_openid_discovery_endpoint/3, merge_openid_configuration/2, merge_oauth_provider/2, extract_ssl_options_as_list/1, - format_ssl_options/1, format_oauth_provider/1, format_oauth_provider_id/1 + format_ssl_options/1, format_oauth_provider/1, format_oauth_provider_id/1, + is_jwt_token/1 ]). -include("oauth2_client.hrl"). -include_lib("kernel/include/logger.hrl"). +-include_lib("jose/include/jose_jwk.hrl"). -spec get_access_token(oauth_provider(), access_token_request()) -> {ok, successful_access_token_response()} | @@ -49,6 +53,123 @@ refresh_access_token(OAuthProvider, Request) -> Response = httpc:request(post, {URL, Header, Type, Body}, HTTPOptions, Options), parse_access_token_response(Response). +-spec introspect_token(binary()) -> + {ok, map()} | + {error, unsuccessful_access_token_response() | any()}. +introspect_token(Token) -> + case build_introspection_request() of + {ok, Request} -> + URL = Request#introspect_token_request.endpoint, + Header = build_introspect_authorization_header_if_any(Request), + Type = ?CONTENT_URLENCODED, + Body = build_introspect_request_parameters(Token, Request), + HTTPOptions = case Request#introspect_token_request.ssl_options of + undefined -> []; + SSL -> [{ssl, SSL}] + end ++ get_default_timeout(), + Options = [], + Response = httpc:request(post, {URL, Header, Type, Body}, HTTPOptions, Options), + case parse_introspect_token_response(Response) of + {error, _} = Error -> + Error; + {ok, _} = Ret -> + ?LOG_DEBUG("Received introspected token: ~p", [Ret]), + Ret + end; + {error, _} = Error -> Error + end. + +-spec sign_token(map()) -> {ok, binary()} | {error, any()}. +sign_token(TokenPayload) -> + case get_opaque_token_signing_key() of + {error, _} = Error -> Error; + {ok, SK} -> + {_, Value} = sign_token_hs(TokenPayload, SK#signing_key.key, SK#signing_key.id), + {ok, Value} + end. + +sign_token_hs(Token, Jwk, TokenKey) -> + Jws0 = #{ + <<"alg">> => <<"HS256">>, + <<"kid">> => TokenKey + }, + Jws = maps:put(<<"kid">>, TokenKey, Jws0), + sign_token(Token, Jwk, Jws). + +sign_token(Token, Jwk, Jws) -> + Signed = jose_jwt:sign(Jwk, Jws, Token), + jose_jws:compact(Signed). + + +build_introspect_request_parameters(Token, #introspect_token_request{ + client_auth_method = Method, + client_id = ClientId, + client_secret = ClientSecret}) -> + QueryList = case Method of + request_param -> [ + client_id_request_parameter(ClientId), + client_secret_request_parameter(ClientSecret) + ]; + _ -> [] + end, + uri_string:compose_query([{?REQUEST_TOKEN, Token} | QueryList]). + + +build_introspect_authorization_header_if_any(#introspect_token_request{ + client_auth_method = Method, + client_id = ClientId, + client_secret = ClientSecret}) -> + case Method of + basic -> + Credentials = erlang:iolist_to_binary([ClientId, <<":">>,ClientSecret ]), + AuthStr = base64:encode_to_string(Credentials), + [{"Authorization", "Basic " ++ AuthStr}]; + _ -> [] + end. + +build_introspection_request() -> + Result = case get_oauth_provider([introspection_endpoint]) of + {ok, Provider} -> + case {Provider#oauth_provider.introspection_client_id, + Provider#oauth_provider.introspection_client_secret} of + {undefined, _} -> {error, not_found_introspection_endpoint}; + {_, undefined} -> {error, not_found_introspection_endpoint}; + {_, _} -> {ok, build_introspection_request(Provider)} + end; + {error, _} = Error -> Error + end, + case Result of + {ok, _} -> Result; + {error, _} -> + Providers = maps:filter(fun(K,_V) -> + case get_oauth_provider(K, [introspection_endpoint]) of + {error, _} -> false; + {ok, P} -> + case {P#oauth_provider.introspection_client_id, + P#oauth_provider.introspection_client_secret, + P#oauth_provider.introspection_endpoint} of + {undefined, _, _} -> false; + {_Id, _Secret, undefined} -> false; + {_Id, _Secret, _Endpoint} -> true + end + end + end, get_env(oauth_providers, #{})), + case maps:size(Providers) of + 0 -> {error, not_found_introspection_endpoint}; + 1 -> {ok, build_introspection_request(lists:last(maps:values(Providers))) }; + _ -> {error, too_many_introspection_endpoints} + end + end. + +build_introspection_request(Provider) -> + #introspect_token_request{ + endpoint = Provider#oauth_provider.introspection_endpoint, + client_id = Provider#oauth_provider.introspection_client_id, + client_secret = Provider#oauth_provider.introspection_client_secret, + client_auth_method = Provider#oauth_provider.introspection_client_auth_method, + ssl_options = Provider#oauth_provider.ssl_options + }. + append_paths(Path1, Path2) -> erlang:iolist_to_binary([Path1, Path2]). @@ -121,10 +242,15 @@ merge_openid_configuration(OpenId, OAuthProvider0) -> EndSessionEndpoint -> OAuthProvider2#oauth_provider{end_session_endpoint = EndSessionEndpoint} end, - case OpenId#openid_configuration.jwks_uri of + OAuthProvider4 = case OpenId#openid_configuration.introspection_endpoint of undefined -> OAuthProvider3; + IntrospectionEndpoint -> + OAuthProvider3#oauth_provider{introspection_endpoint = IntrospectionEndpoint} + end, + case OpenId#openid_configuration.jwks_uri of + undefined -> OAuthProvider4; JwksUri -> - OAuthProvider3#oauth_provider{jwks_uri = JwksUri} + OAuthProvider4#oauth_provider{jwks_uri = JwksUri} end. -spec merge_oauth_provider(oauth_provider(), proplists:proplist()) -> @@ -145,10 +271,15 @@ merge_oauth_provider(OAuthProvider, Proplist) -> EndSessionEndpoint -> [{end_session_endpoint, EndSessionEndpoint} | proplists:delete(end_session_endpoint, Proplist1)] end, - case OAuthProvider#oauth_provider.jwks_uri of + Proplist3 = case OAuthProvider#oauth_provider.introspection_endpoint of undefined -> Proplist2; + IntrospectionEndpoint -> [{introspection_endpoint, IntrospectionEndpoint} | + proplists:delete(introspection_endpoint, Proplist2)] + end, + case OAuthProvider#oauth_provider.jwks_uri of + undefined -> Proplist3; JwksEndPoint -> [{jwks_uri, JwksEndPoint} | - proplists:delete(jwks_uri, Proplist2)] + proplists:delete(jwks_uri, Proplist3)] end. parse_openid_configuration_response({error, Reason}) -> @@ -177,6 +308,8 @@ map_to_openid_configuration(Map) -> Map, undefined), end_session_endpoint = maps:get(?RESPONSE_END_SESSION_ENDPOINT, Map, undefined), + introspection_endpoint = maps:get(?RESPONSE_INTROSPECTION_ENDPOINT, + Map, undefined), jwks_uri = maps:get(?RESPONSE_JWKS_URI, Map, undefined) }. @@ -216,6 +349,10 @@ do_update_oauth_provider_endpoints_configuration(OAuthProvider) when undefined -> do_nothing; EndSessionEndpoint -> set_env(end_session_endpoint, EndSessionEndpoint) end, + case OAuthProvider#oauth_provider.introspection_endpoint of + undefined -> do_nothing; + IntrospectionEndpoint -> set_env(introspection_endpoint, IntrospectionEndpoint) + end, case OAuthProvider#oauth_provider.jwks_uri of undefined -> do_nothing; JwksUri -> set_env(jwks_uri, JwksUri) @@ -267,6 +404,76 @@ unlock(LockId) -> end end. +-spec get_opaque_token_signing_key() -> {ok, signing_key()} | {error, any()}. +get_opaque_token_signing_key() -> + case get_env(opaque_token_signing_key) of + undefined -> {error, missing_opaque_token_signing_key}; + List -> + {ok, parse_signing_key_configuration(List)} + end. + +-spec get_opaque_token_signing_key(string()|binary()) -> {ok, signing_key()} | {error, any()}. +get_opaque_token_signing_key(KeyId) -> + case get_env(opaque_token_signing_key) of + undefined -> {error, missing_opaque_token_signing_key}; + List -> + case proplists:get_value(id, List, undefined) of + undefined -> {error, missing_opaque_token_signing_key}; + KeyId -> {ok, parse_signing_key_configuration(List)}; + _ -> {error, missing_opaque_token_signing_key} + end + end. + +parse_signing_key_configuration(List) -> + SK0 = case proplists:get_value(id, List, undefined) of + undefined -> {error, missing_signing_key_id}; + Id -> #signing_key{id = Id, type = hs256} + end, + case {SK0, proplists:get_value(type, List, hs256)} of + {{error, _} = Error, _} -> + Error; + {_, hs256} -> + SK1OrError = case proplists:get_value(key, List, undefined) of + undefined -> {error, missing_symmetrical_key_value}; + SymKey -> + case make_jwk(#{ + <<"alg">> => <<"HS256">>, + <<"value">> => SymKey, + <<"kty">> => <<"MAC">>, + <<"use">> => <<"sig">>}) of + {error, _} = Error -> Error; + {ok, Val} -> + SK0#signing_key{ + key = Val + } + end + end, + case SK1OrError of + {error, _} = Error1 -> Error1; + SK1 -> SK1 + end; +% {_, rs256} -> +% Sk2 = case proplists:get_value(key_pem_file, List, undefined) of +% undefined -> +% {error, missing_key_pem_file}; +% PrivateKey -> +% case proplists:get_value(cert_pem_file, List, undefined) of +% undefined -> +% {error, missing_cert_pem_file}; +% PublicKey -> +% SK0#signing_key{type = hs256, +% private_key = PrivateKey, +% public_key = PublicKey} +% end +% end, +% case {Sk2#signing_key.private_key, Sk2#signing_key.public_key} of +% {{error, _} = Error2, _} -> Error2; +% {_, {error, _} = Error3} -> Error3; +% {_, _} -> Sk2 +% end; + {_, _} -> {error, unsupported_signing_type} + end. + -spec get_oauth_provider(list()) -> {ok, oauth_provider()} | {error, any()}. get_oauth_provider(ListOfRequiredAttributes) -> case get_env(default_oauth_provider) of @@ -396,6 +603,10 @@ lookup_root_oauth_provider() -> token_endpoint = get_env(token_endpoint), authorization_endpoint = get_env(authorization_endpoint), end_session_endpoint = get_env(end_session_endpoint), + introspection_endpoint = get_env(introspection_endpoint), + introspection_client_auth_method = get_env(introspection_client_auth_method), + introspection_client_id = get_env(introspection_client_id), + introspection_client_secret = get_env(introspection_client_secret), ssl_options = extract_ssl_options_as_list(Map) }. @@ -498,13 +709,17 @@ build_refresh_token_request_body(Request) -> grant_type_request_parameter(Type) -> {?REQUEST_GRANT_TYPE, Type}. +client_id_request_parameter(ClientId) when is_binary(ClientId) -> + {?REQUEST_CLIENT_ID, binary_to_list(ClientId)}; + client_id_request_parameter(ClientId) -> - {?REQUEST_CLIENT_ID, - binary_to_list(ClientId)}. + {?REQUEST_CLIENT_ID, ClientId}. + +client_secret_request_parameter(ClientSecret)when is_binary(ClientSecret) -> + {?REQUEST_CLIENT_SECRET, binary_to_list(ClientSecret)}; -client_secret_request_parameter(ClientSecret) -> - {?REQUEST_CLIENT_SECRET, - binary_to_list(ClientSecret)}. +client_secret_request_parameter(ClientSecret) -> + {?REQUEST_CLIENT_SECRET, ClientSecret}. refresh_token_request_parameter(Request) -> {?REQUEST_REFRESH_TOKEN, Request#refresh_token_request.refresh_token}. @@ -530,9 +745,11 @@ get_ssl_options_if_any(OAuthProvider) -> end. get_timeout_of_default(Timeout) -> case Timeout of - undefined -> [{timeout, ?DEFAULT_HTTP_TIMEOUT}]; + undefined -> get_default_timeout(); Timeout -> [{timeout, Timeout}] end. +get_default_timeout() -> + [{timeout, ?DEFAULT_HTTP_TIMEOUT}]. is_json(?CONTENT_JSON) -> true; is_json(_) -> false. @@ -585,6 +802,14 @@ map_to_oauth_provider(PropList) when is_list(PropList) -> proplists:get_value(authorization_endpoint, PropList, undefined), end_session_endpoint = proplists:get_value(end_session_endpoint, PropList, undefined), + introspection_endpoint = + proplists:get_value(introspection_endpoint, PropList, undefined), + introspection_client_id = + proplists:get_value(introspection_client_id, PropList, undefined), + introspection_client_secret = + proplists:get_value(introspection_client_secret, PropList, undefined), + introspection_client_auth_method = + proplists:get_value(introspection_client_auth_method, PropList, basic), jwks_uri = proplists:get_value(jwks_uri, PropList, undefined), ssl_options = @@ -607,11 +832,48 @@ map_to_access_token_response(Code, Reason, Headers, Body) -> _ -> {error, Reason} end end. +map_to_introspect_token_response(Code, Reason, Headers, Body) -> + case decode_body(proplists:get_value("content-type", Headers, ?CONTENT_JSON), Body) of + {error, {error, InternalError}} -> + {error, InternalError}; + {error, _} = Error -> + Error; + Value -> + case Code of + 200 -> assert_token_is_active({ok, Value}); + 201 -> assert_token_is_active({ok, Value}); + 204 -> {ok, []}; + 400 -> {error, map_to_unsuccessful_introspect_token_response(Value)}; + 401 -> {error, map_to_unsuccessful_introspect_token_response(Value)}; + _ -> {error, Reason} + end + end. +assert_token_is_active({ok, Response} = Value) -> + case maps:get(<<"active">>, Response, undefined) of + undefined -> {error, introspected_token_not_valid}; + false -> {error, introspected_token_not_valid}; + true -> Value + end. + +map_to_unsuccessful_introspect_token_response(Map) when is_map(Map) -> + #unsuccessful_introspect_token_response{ + error = maps:get(?RESPONSE_ERROR, Map, "unknown"), + error_description = maps:get(?RESPONSE_ERROR_DESCRIPTION, Map, undefined) + }; +map_to_unsuccessful_introspect_token_response(_) -> + #unsuccessful_introspect_token_response{ + error = "unknown" + }. parse_access_token_response({error, Reason}) -> {error, Reason}; parse_access_token_response({ok,{{_,Code,Reason}, Headers, Body}}) -> map_to_access_token_response(Code, Reason, Headers, Body). +parse_introspect_token_response({error, Reason}) -> + {error, Reason}; +parse_introspect_token_response({ok,{{_,Code,Reason}, Headers, Body}}) -> + map_to_introspect_token_response(Code, Reason, Headers, Body). + -spec format_ssl_options([ssl:tls_client_option()]) -> string(). format_ssl_options(TlsOptions) -> CaCertsCount = case proplists:get_value(cacerts, TlsOptions, []) of @@ -635,13 +897,19 @@ format_oauth_provider(OAuthProvider) -> lists:flatten(io_lib:format("{id: ~p, issuer: ~p, discovery_endpoint: ~p, " ++ " token_endpoint: ~p, " ++ "authorization_endpoint: ~p, end_session_endpoint: ~p, " ++ - "jwks_uri: ~p, ssl_options: ~p }", [ + "introspection{endpoint: ~p, client_id: ~p, has client_secret: ~p} jwks_uri: ~p, ssl_options: ~p }", [ format_oauth_provider_id(OAuthProvider#oauth_provider.id), OAuthProvider#oauth_provider.issuer, OAuthProvider#oauth_provider.discovery_endpoint, OAuthProvider#oauth_provider.token_endpoint, OAuthProvider#oauth_provider.authorization_endpoint, OAuthProvider#oauth_provider.end_session_endpoint, + OAuthProvider#oauth_provider.introspection_endpoint, + OAuthProvider#oauth_provider.introspection_client_id, + case OAuthProvider#oauth_provider.introspection_client_secret of + undefined -> false; + _ -> true + end, OAuthProvider#oauth_provider.jwks_uri, format_ssl_options(OAuthProvider#oauth_provider.ssl_options)])). @@ -651,3 +919,48 @@ get_env(Par, Def) -> application:get_env(rabbitmq_auth_backend_oauth2, Par, Def). set_env(Par, Val) -> application:set_env(rabbitmq_auth_backend_oauth2, Par, Val). + +-spec is_jwt_token(list() | binary() | map()) -> boolean(). +is_jwt_token(Token) when is_list(Token) -> + is_jwt_token(list_to_binary(Token)); +is_jwt_token(Token) when is_binary(Token) -> + case binary:split(Token, <<".">>, [global]) of + [_, _, _] -> true; + _ -> false + end; +is_jwt_token(_Token) -> true. + +-spec make_jwk(map()) -> {ok, #{binary() => binary()}} | {error, term()}. + +make_jwk(JsonMap) when is_map(JsonMap) -> + case JsonMap of + #{<<"kty">> := <<"MAC">>, <<"value">> := _Value} -> + {ok, mac_to_oct(JsonMap)}; + #{<<"kty">> := _Kty} -> + {error, unknown_kty}; + #{} -> + {error, no_kty} + end. + +mac_to_oct(#{<<"kty">> := <<"MAC">>, <<"value">> := Value} = Key) -> + OktKey = maps:merge(Key, + #{<<"kty">> => <<"oct">>, + <<"k">> => base64: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() -> + UaaEnv = application:get_env(rabbitmq_auth_backend_oauth2, uaa_jwt_decoder, []), + DefaultAlgs = #{<<"HMACSHA256">> => <<"HS256">>, + <<"HMACSHA384">> => <<"HS384">>, + <<"HMACSHA512">> => <<"HS512">>, + <<"SHA256withRSA">> => <<"RS256">>, + <<"SHA512withRSA">> => <<"RS512">>}, + proplists:get_value(uaa_algs, UaaEnv, DefaultAlgs). diff --git a/deps/oauth2_client/test/oauth_http_mock.erl b/deps/oauth2_client/test/oauth_http_mock.erl index c1a6710104bf..493b80ed31eb 100644 --- a/deps/oauth2_client/test/oauth_http_mock.erl +++ b/deps/oauth2_client/test/oauth_http_mock.erl @@ -32,8 +32,13 @@ match_request(Req, #{method := Method} = ExpectedRequest) -> case maps:is_key(parameters, ExpectedRequest) of true -> match_request_parameters_in_body(Req, ExpectedRequest); false -> ok + end, + case maps:is_key(headers, ExpectedRequest) of + true -> maps:foreach(fun(K,V) -> + ?assertEqual(V, cowboy_req:header(K, Req)) end, + maps:get(headers, ExpectedRequest)); + false -> ok end. - produce_expected_response(ExpectedResponse) -> case proplists:is_defined(content_type, ExpectedResponse) of true -> diff --git a/deps/oauth2_client/test/system_SUITE.erl b/deps/oauth2_client/test/system_SUITE.erl index 8153fd73d895..2a375e37b57c 100644 --- a/deps/oauth2_client/test/system_SUITE.erl +++ b/deps/oauth2_client/test/system_SUITE.erl @@ -1,4 +1,3 @@ -%% This Source Code Form is subject to the terms of the Mozilla Public %% License, v. 2.0. If a copy of the MPL was not distributed with this %% file, You can obtain one at https://mozilla.org/MPL/2.0/. %% @@ -17,6 +16,8 @@ -compile(export_all). +-define(MOCK_OPAQUE_TOKEN, <<"some opaque token">>). +-define(MOCK_INTROSPECTION_ENDPOINT, <<"/introspect">>). -define(MOCK_TOKEN_ENDPOINT, <<"/token">>). -define(AUTH_PORT, 8000). -define(ISSUER_PATH, "/somepath"). @@ -28,7 +29,8 @@ all() -> [ {group, https_down}, {group, https}, - {group, with_all_oauth_provider_settings} + {group, with_all_oauth_provider_settings}, + {group, verify_introspect_token} ]. @@ -40,6 +42,44 @@ groups() -> jwks_uri_takes_precedence_over_jwks_url, jwks_url_is_used_in_absense_of_jwks_uri ]}, + {verify_introspect_token, [], [ + {with_all_oauth_provider_settings, [], [ + cannot_introspect_due_to_missing_configuration, + {with_introspection_endpoint, [], [ + cannot_introspect_due_to_missing_configuration, + {https, [], [ + {with_introspection_basic_client_credentials, [], [ + can_introspect_token + ]}, + {with_introspection_request_param_client_credentials, [], [ + can_introspect_token + ]}, + {introspection_endpoint_returns_non_active_tokens, [], [ + introspected_token_is_not_active + ]} + ]} + ]}, + {https, [], [ + {with_introspection_basic_client_credentials, [], [ + cannot_introspect_due_to_missing_configuration + ]}, + {with_introspection_request_param_client_credentials, [], [ + cannot_introspect_due_to_missing_configuration + ]} + ]}, + {with_discovered_introspection_endpoint, [], [ + cannot_introspect_due_to_missing_configuration, + {https, [], [ + {with_introspection_basic_client_credentials, [], [ + can_introspect_token + ]}, + {with_introspection_request_param_client_credentials, [], [ + can_introspect_token + ]} + ]} + ]} + ]} + ]}, {without_all_oauth_providers_settings, [], [ {group, verify_get_oauth_provider} ]}, @@ -152,6 +192,74 @@ init_per_group(with_default_oauth_provider, Config) -> OAuthProvider#oauth_provider.id), Config; +init_per_group(with_hs256_signing, Config) -> + application:set_env(rabbitmq_auth_backend_oauth2, opaque_token_signing_key, + #{ id => <<"some-id">>, + type => hs256, + key => <<"some-key-value">> }), + Config; + +init_per_group(with_introspection_endpoint, Config) -> + application:set_env(rabbitmq_auth_backend_oauth2, introspection_endpoint, + build_token_introspection_endpoint("https")), + Config; + +init_per_group(with_discovered_introspection_endpoint, Config) -> + Payload1 = [ {?RESPONSE_INTROSPECTION_ENDPOINT, build_token_introspection_endpoint("https")} | + build_http_get_openid_configuration_payload() ], + [{expected_openid_configuration_payload, Payload1} | Config]; + +init_per_group(with_introspection_basic_client_credentials, Config) -> + application:set_env(rabbitmq_auth_backend_oauth2, introspection_client_id, + "some-client-id"), + application:set_env(rabbitmq_auth_backend_oauth2, introspection_client_secret, + "some-client-secret"), + application:set_env(rabbitmq_auth_backend_oauth2, introspection_client_auth_method, + basic), + [{can_introspect_token, [ + {introspection_endpoint, build_http_mock_behaviour( + build_introspection_token_request(?MOCK_OPAQUE_TOKEN, basic, <<"some-client-id">>, + <<"some-client-secret">>), + build_http_200_introspection_token_response())}, + {get_openid_configuration, get_openid_configuration_http_expectation( + with_introspection_basic_client_credentials, Config)} + + ]} | Config]; +init_per_group(introspection_endpoint_returns_non_active_tokens, Config) -> + application:set_env(rabbitmq_auth_backend_oauth2, introspection_client_id, + "some-client-id"), + application:set_env(rabbitmq_auth_backend_oauth2, introspection_client_secret, + "some-client-secret"), + application:set_env(rabbitmq_auth_backend_oauth2, introspection_client_auth_method, + basic), + [{introspected_token_is_not_active, [ + {introspection_endpoint, build_http_mock_behaviour( + build_introspection_token_request(?MOCK_OPAQUE_TOKEN, basic, <<"some-client-id">>, + <<"some-client-secret">>), + build_http_200_introspection_token_response([ + {active, false}, + {scope, <<"openid">>} + ]))}, + {get_openid_configuration, get_openid_configuration_http_expectation( + with_introspection_basic_client_credentials, Config)} + + ]} | Config]; + +init_per_group(with_introspection_request_param_client_credentials, Config) -> + application:set_env(rabbitmq_auth_backend_oauth2, introspection_client_id, + "some-client-id"), + application:set_env(rabbitmq_auth_backend_oauth2, introspection_client_secret, + "some-client-secret"), + application:set_env(rabbitmq_auth_backend_oauth2, introspection_client_auth_method, + request_param), + [{can_introspect_token, [ + {introspection_endpoint, build_http_mock_behaviour( + build_introspection_token_request(?MOCK_OPAQUE_TOKEN, request_param, <<"some-client-id">>, + <<"some-client-secret">>), + build_http_200_introspection_token_response())} + ]} | Config]; + + init_per_group(_, Config) -> Config. @@ -161,20 +269,24 @@ get_http_oauth_server_expectations(TestCase, Config) -> undefined -> [ {token_endpoint, build_http_mock_behaviour(build_http_access_token_request(), build_http_200_access_token_response())}, - {get_openid_configuration, get_openid_configuration_http_expectation(TestCase)} + {get_openid_configuration, get_openid_configuration_http_expectation(TestCase, Config)} ]; Expectations -> Expectations end. -get_openid_configuration_http_expectation(TestCaseAtom) -> +get_openid_configuration_http_expectation(TestCaseAtom, Config) -> TestCase = binary_to_list(atom_to_binary(TestCaseAtom)), - Payload = case string:find(TestCase, "returns_partial_payload") of - nomatch -> - build_http_get_openid_configuration_payload(); - _ -> - List0 = proplists:delete(authorization_endpoint, - build_http_get_openid_configuration_payload()), - proplists:delete(end_session_endpoint, List0) + Payload = case ?config(expected_openid_configuration_payload, Config) of + undefined -> + case string:find(TestCase, "returns_partial_payload") of + nomatch -> + build_http_get_openid_configuration_payload(); + _ -> + List0 = proplists:delete(authorization_endpoint, + build_http_get_openid_configuration_payload()), + proplists:delete(end_session_endpoint, List0) + end; + P -> P end, Path = case string:find(TestCase, "path") of nomatch -> ""; @@ -191,7 +303,6 @@ lookup_expectation(Endpoint, Config) -> proplists:get_value(Endpoint, ?config(oauth_server_expectations, Config)). - configure_all_oauth_provider_settings(Config) -> OAuthProvider = ?config(oauth_provider, Config), OAuthProviders = #{ ?config(oauth_provider_id, Config) => @@ -311,6 +422,22 @@ end_per_group(with_default_oauth_provider, Config) -> application:unset_env(rabbitmq_auth_backend_oauth2, default_oauth_provider), Config; +end_per_group(with_introspection_endpoint, Config) -> + application:unset_env(rabbitmq_auth_backend_oauth2, introspection_endpoint), + Config; + +end_per_group(with_introspection_basic_client_credentials, Config) -> + application:unset_env(rabbitmq_auth_backend_oauth2, introspection_endpoint_client_id), + application:unset_env(rabbitmq_auth_backend_oauth2, introspection_endpoint_client_secret), + application:unset_env(rabbitmq_auth_backend_oauth2, introspection_endpoint_client_auth_method), + Config; + +end_per_group(with_introspection_request_param_client_credentials, Config) -> + application:unset_env(rabbitmq_auth_backend_oauth2, introspection_endpoint_client_id), + application:unset_env(rabbitmq_auth_backend_oauth2, introspection_endpoint_client_secret), + application:unset_env(rabbitmq_auth_backend_oauth2, introspection_endpoint_client_auth_method), + Config; + end_per_group(_, Config) -> Config. @@ -598,19 +725,38 @@ get_oauth_provider_given_oauth_provider_id(Config) -> Jwks_uri) end. -jwks_url_is_used_in_absense_of_jwks_uri(Config) -> +jwks_url_is_used_in_absense_of_jwks_uri(_Config) -> {ok, #oauth_provider{ jwks_uri = Jwks_uri}} = oauth2_client:get_oauth_provider([jwks_uri]), ?assertEqual( proplists:get_value(jwks_url, get_env(key_config, []), undefined), Jwks_uri). -jwks_uri_takes_precedence_over_jwks_url(Config) -> +jwks_uri_takes_precedence_over_jwks_url(_Config) -> {ok, #oauth_provider{ jwks_uri = Jwks_uri}} = oauth2_client:get_oauth_provider([jwks_uri]), ?assertEqual(get_env(jwks_uri), Jwks_uri). +cannot_introspect_due_to_missing_configuration(_Config)-> + {error, not_found_introspection_endpoint} = oauth2_client:introspect_token(?MOCK_OPAQUE_TOKEN), + + application:set_env(rabbitmq_auth_backend_oauth2, introspection_client_id, "some-client-id"), + {error, not_found_introspection_endpoint} = oauth2_client:introspect_token(?MOCK_OPAQUE_TOKEN), + application:unset_env(rabbitmq_auth_backend_oauth2, introspection_client_id), + + application:set_env(rabbitmq_auth_backend_oauth2, introspection_client_secret, "some-client-secret"), + {error, not_found_introspection_endpoint} = oauth2_client:introspect_token(?MOCK_OPAQUE_TOKEN), + application:unset_env(rabbitmq_auth_backend_oauth2, introspection_client_secret). + +can_introspect_token(_Config) -> + {ok, Value} = oauth2_client:introspect_token(?MOCK_OPAQUE_TOKEN), + ct:log("JWT : ~p", [Value]), + ok. + +introspected_token_is_not_active(_Config) -> + {error, introspected_token_not_valid} = oauth2_client:introspect_token(?MOCK_OPAQUE_TOKEN). + %%% HELPERS build_issuer(Scheme) -> @@ -637,6 +783,12 @@ build_jwks_uri(Scheme, Path) -> port => rabbit_data_coercion:to_integer(?AUTH_PORT), path => Path}). +build_token_introspection_endpoint(Scheme) -> + uri_string:recompose(#{scheme => Scheme, + host => "localhost", + port => rabbit_data_coercion:to_integer(?AUTH_PORT), + path => "/introspect"}). + build_access_token_request(Request) -> #access_token_request { client_id = proplists:get_value(?REQUEST_CLIENT_ID, Request), @@ -664,6 +816,8 @@ build_https_oauth_provider(Id, CaCertFile) -> jwks_uri = build_jwks_uri("https"), ssl_options = ssl_options(verify_peer, false, CaCertFile) }. +oauth_provider_to_proplist(undefined) -> []; + oauth_provider_to_proplist(#oauth_provider{ issuer = Issuer, token_endpoint = TokenEndpoint, @@ -688,6 +842,7 @@ start_https_oauth_server(Port, CertsDir, Expectations) when is_list(Expectations {'_', [{Path, oauth_http_mock, Expected} || #{request := #{path := Path}} = Expected <- Expectations ]} ]), + ct:log("start_https_oauth_server with Expectations: ~p", [Expectations]), {ok, _} = cowboy:start_tls( mock_http_auth_listener, [{port, Port}, @@ -816,6 +971,38 @@ denies_access_token_expectation() -> {?REQUEST_CLIENT_SECRET, <<"password">>} ]), build_http_400_access_token_response() ). +build_introspection_token_request(Token, basic, ClientId, ClientSecret) -> + Map = build_http_request( + <<"POST">>, + ?MOCK_INTROSPECTION_ENDPOINT, + [ + {?REQUEST_TOKEN, Token} + ]), + Credentials = binary_to_list(<>), + AuthStr = base64:encode_to_string(Credentials), + maps:put(headers, #{ + <<"authorization">> => list_to_binary("Basic " ++ AuthStr) + }, Map); +build_introspection_token_request(Token, request_param, ClientId, ClientSecret) -> + build_http_request( + <<"POST">>, + ?MOCK_INTROSPECTION_ENDPOINT, + [ + {?REQUEST_TOKEN, Token}, + {?REQUEST_CLIENT_ID, ClientId}, + {?REQUEST_CLIENT_SECRET, ClientSecret} + ]). +build_http_200_introspection_token_response() -> + build_http_200_introspection_token_response([ + {active, true}, + {scope, <<"openid">>} + ]). +build_http_200_introspection_token_response(PayloodList) -> + [ + {code, 200}, + {content_type, ?CONTENT_JSON}, + {payload, PayloodList} + ]. auth_server_error_when_access_token_request_expectation() -> build_http_mock_behaviour(build_http_request( <<"POST">>, diff --git a/deps/oauth2_client/test/unit_SUITE.erl b/deps/oauth2_client/test/unit_SUITE.erl index dfdf517a721d..bb6a65dcdbd4 100644 --- a/deps/oauth2_client/test/unit_SUITE.erl +++ b/deps/oauth2_client/test/unit_SUITE.erl @@ -24,11 +24,17 @@ all() -> build_openid_discovery_endpoint, {group, ssl_options}, {group, merge}, - {group, get_expiration_time} + {group, get_expiration_time}, + {group, sign_token} ]. groups() -> [ + {sign_token, [], [ + can_sign_token, + is_jwt_token, + is_not_jwt_token + ]}, {ssl_options, [], [ no_ssl_options_triggers_verify_peer, choose_verify_over_peer_verification, @@ -109,6 +115,15 @@ merge_oauth_provider(_) -> {token_endpoint, OAuthProvider4#oauth_provider.token_endpoint}], Proplist5), + OAuthProvider5 = OAuthProvider4#oauth_provider{introspection_endpoint = "https://introspection"}, + Proplist6 = oauth2_client:merge_oauth_provider(OAuthProvider5, Proplist5), + ?assertEqual([{jwks_uri, OAuthProvider5#oauth_provider.jwks_uri}, + {introspection_endpoint, OAuthProvider5#oauth_provider.introspection_endpoint}, + {end_session_endpoint, OAuthProvider5#oauth_provider.end_session_endpoint}, + {authorization_endpoint, OAuthProvider5#oauth_provider.authorization_endpoint}, + {token_endpoint, OAuthProvider5#oauth_provider.token_endpoint}], + Proplist6), + % ensure id, issuer, ssl_options and discovery_endpoint are not affected ?assertEqual(OAuthProvider#oauth_provider.id, OAuthProvider4#oauth_provider.id), @@ -287,5 +302,20 @@ access_token_response_without_expiration_time(_) -> AccessTokenResponse = #successful_access_token_response{ access_token = EncodedToken }, - ct:log("AccessTokenResponse ~p", [AccessTokenResponse]), ?assertEqual({error, missing_exp_field}, oauth2_client:get_expiration_time(AccessTokenResponse)). + + +can_sign_token(_Config) -> + application:set_env(rabbitmq_auth_backend_oauth2, opaque_token_signing_key, + [{ id, <<"key-id">>}, {type, hs256}, {key, <<"some-key">>}]), + + {ok, _ } = oauth2_client:sign_token(#{"scopes" => "a b"}). + +is_jwt_token(Config) -> + Jwk = ?UTIL_MOD:fixture_jwk(), + AccessToken = maps:remove(<<"exp">>, ?UTIL_MOD:fixture_token()), + {_, EncodedToken} = ?UTIL_MOD:sign_token_hs(AccessToken, Jwk), + ?assertEqual(true, oauth2_client:is_jwt_token(EncodedToken)). + +is_not_jwt_token(_) -> + ?assertEqual(false, oauth2_client:is_jwt_token(<<"some opaque token">>)). diff --git a/deps/rabbitmq_auth_backend_oauth2/include/oauth2.hrl b/deps/rabbitmq_auth_backend_oauth2/include/oauth2.hrl index e7792e49298b..c3ffe1a0db4e 100644 --- a/deps/rabbitmq_auth_backend_oauth2/include/oauth2.hrl +++ b/deps/rabbitmq_auth_backend_oauth2/include/oauth2.hrl @@ -16,6 +16,7 @@ %% Key JWT fields %% +-define(ACTIVE_FIELD, <<"active">>). %% FOR INTROSPECTED TOKENS -define(AUD_JWT_FIELD, <<"aud">>). -define(SCOPE_JWT_FIELD, <<"scope">>). -define(TAG_SCOPE_PREFIX, <<"tag:">>). diff --git a/deps/rabbitmq_auth_backend_oauth2/priv/schema/rabbitmq_auth_backend_oauth2.schema b/deps/rabbitmq_auth_backend_oauth2/priv/schema/rabbitmq_auth_backend_oauth2.schema index 222b1dedfb21..9aa176768aed 100644 --- a/deps/rabbitmq_auth_backend_oauth2/priv/schema/rabbitmq_auth_backend_oauth2.schema +++ b/deps/rabbitmq_auth_backend_oauth2/priv/schema/rabbitmq_auth_backend_oauth2.schema @@ -122,6 +122,74 @@ [list_to_binary(V) || {_, V} <- lists:reverse(Settings)] end}. +%% Signing key used by RabbitMQ to convert an introspected opaque token +%% into a JWT token for the management UI use case. For messaging +%% use cases like AMQP it is not necessary because when a user authenticates with +%% an opaque token, RabbitMQ introspects the opaque token to obtain the actual +%% JWT's payload with the scopes and those scopes are kept along with the connection +%% in RabbitMQ's memory for as long as the connection stays alive, exactly as if the user +%% would have presented a JWT from the beginning. +%% For the management UI use case, there is no server-side state +%% hence the management UI has to convert an opaque token into a +%% JWT token so that RabbitMQ can validate the JWT token without making +%% an external HTTP request to the introspection endpoint. If the management ui +%% sends an opaque token, RabbitMQ, in order to validate the token, has to +%% introspect it. +%% +%% Here it is how this signing key is used: +%% When a management user logs in for the first time with an +%% opaque token, the token is instrospected and validated it. +%% If the token is valid, RabbitMQ issues a JWT token whose payload +%% is the introspected token and the signing key is configured in +%% `auth_oauth2.opaque_token_signing_key`. +%% +%% The issued JWT token has the same expiry date, scopes, etc as the original +%% opaque token. In other words, RabbitMQ does not add anything. +%% It only wraps it with a digital signature. +%% +%% Maybe it is necessary a rabbitmqctl command to rotate the signing key like: +%% rabbitmqctl rotate_opaque_token_signing_key [ | ] +%% +%% Note: This feature is only necessary when the management UI needs to authenticate with OAuth2 +%% using opaque tokens. In all other cases, this feature is not necessary. +%% +%% Example: +%% auth_oauth2.opaque_token_signing_key.id = rabbit_kid +%% for symmetrical key +%% auth_oauth2.opaque_token_signing_key.type = HS256 +%% auth_oauth2.opaque_token_signing_key.key = "hello-world-symmetrical-key" +%% for asymmetrical key +%% auth_oauth2.opaque_token_signing_key.type = RS256 +%% auth_oauth2.opaque_token_signing_key.key_pem_file = rabbit_key.pem +%% auth_oauth2.opaque_token_signing_key.cert_pem_file = rabbit_cert.pem + +{mapping, "auth_oauth2.opaque_token_signing_key.id", + "rabbitmq_auth_backend_oauth2.opaque_token_signing_key.id", + [{datatype, string}]}. + +{translation, + "rabbitmq_auth_backend_oauth2.opaque_token_signing_key.id", + fun(Conf) -> list_to_binary(cuttlefish:conf_get("auth_oauth2.opaque_token_signing_key.id", Conf)) end}. + +{mapping, "auth_oauth2.opaque_token_signing_key.type", + "rabbitmq_auth_backend_oauth2.opaque_token_signing_key.type", + [{datatype, {enum, [hs256, rs256]}}]}. + +{mapping, "auth_oauth2.opaque_token_signing_key.key", + "rabbitmq_auth_backend_oauth2.opaque_token_signing_key.key", + [{datatype, string}]}. + +{translation, + "rabbitmq_auth_backend_oauth2.opaque_token_signing_key.key", + fun(Conf) -> list_to_binary(cuttlefish:conf_get("auth_oauth2.opaque_token_signing_key.key", Conf)) end}. + +{mapping, "auth_oauth2.opaque_token_signing_key.key_file", + "rabbitmq_auth_backend_oauth2.opaque_token_signing_key.key_pem_file", + [{datatype, file}, {validators, ["file_accessible"]}]}. + +{mapping, "auth_oauth2.opaque_token_signing_key.cert_file", + "rabbitmq_auth_backend_oauth2.opaque_token_signing_key.cert_pem_file", + [{datatype, file}, {validators, ["file_accessible"]}]}. %% ID of the default signing key @@ -153,6 +221,33 @@ rabbit_oauth2_schema:translate_signing_keys(Conf) end}. +%% basic_authorization -> Authorization: Basic base64(client_id, client_secret) +%% post_request_param -> &client_id=&client_secret= +{mapping, + "auth_oauth2.introspection_client_auth_method", + "rabbitmq_auth_backend_oauth2.introspection_client_auth_method", + [{datatype, {enum, [basic, request_param]}}]}. + +{mapping, + "auth_oauth2.introspection_client_id", + "rabbitmq_auth_backend_oauth2.introspection_client_id", + [{datatype, string}]}. + +{translation, + "rabbitmq_auth_backend_oauth2.introspection_client_id", + fun(Conf) -> list_to_binary(cuttlefish:conf_get("auth_oauth2.introspection_client_id", Conf)) + end}. + +{mapping, + "auth_oauth2.introspection_client_secret", + "rabbitmq_auth_backend_oauth2.introspection_client_secret", +[{datatype, string}]}. + +{translation, + "rabbitmq_auth_backend_oauth2.introspection_client_secret", + fun(Conf) -> list_to_binary(cuttlefish:conf_get("auth_oauth2.introspection_client_secret", Conf)) + end}. + {mapping, "auth_oauth2.issuer", "rabbitmq_auth_backend_oauth2.issuer", @@ -200,6 +295,11 @@ rabbit_oauth2_schema:translate_endpoint_params("discovery_endpoint_params", Conf) end}. +{mapping, + "auth_oauth2.introspection_endpoint", + "rabbitmq_auth_backend_oauth2.introspection_endpoint", + [{datatype, string}, {validators, ["uri", "https_uri"]}]}. + {mapping, "auth_oauth2.oauth_providers.$name.discovery_endpoint_params.$param", "rabbitmq_auth_backend_oauth2.oauth_providers", @@ -291,6 +391,12 @@ [{datatype, string}, {validators, ["uri", "https_uri"]}] }. +{mapping, + "auth_oauth2.oauth_providers.$name.introspection_endpoint", + "rabbitmq_auth_backend_oauth2.oauth_providers", + [{datatype, string}, {validators, ["uri", "https_uri"]}] +}. + {mapping, "auth_oauth2.oauth_providers.$name.jwks_uri", "rabbitmq_auth_backend_oauth2.oauth_providers", @@ -307,6 +413,23 @@ "rabbitmq_auth_backend_oauth2.oauth_providers", [{datatype, string}, {validators, ["uri", "https_uri"]}]}. +%% basic_authorization -> Authorization: Basic base64(client_id, client_secret) +%% post_request_param -> &client_id=&client_secret= +{mapping, + "auth_oauth2.oauth_providers.$name.introspection_client_auth_method", + "rabbitmq_auth_backend_oauth2.oauth_providers", + [{datatype, {enum, [basic, request_param]}}]}. + +{mapping, + "auth_oauth2.oauth_providers.$name.introspection_client_id", + "rabbitmq_auth_backend_oauth2.oauth_providers", + [{datatype, string}]}. + +{mapping, + "auth_oauth2.oauth_providers.$name.introspection_client_secret", + "rabbitmq_auth_backend_oauth2.oauth_providers", +[{datatype, string}]}. + {mapping, "auth_oauth2.oauth_providers.$name.https.verify", "rabbitmq_auth_backend_oauth2.oauth_providers", @@ -408,7 +531,6 @@ "rabbitmq_auth_backend_oauth2.resource_servers", [{datatype, string}]}. - {translation, "rabbitmq_auth_backend_oauth2.resource_servers", fun(Conf) -> rabbit_oauth2_schema:translate_resource_servers(Conf) diff --git a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl index 1cbaadbb086f..666c384c3283 100644 --- a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl +++ b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl @@ -110,29 +110,47 @@ check_topic_access(#auth_user{impl = DecodedTokenFun}, end). update_state(AuthUser, NewToken) -> - case resolve_resource_server(NewToken) of - {error, _} = Err0 -> Err0; - {ResourceServer, _} = Tuple -> - case check_token(NewToken, Tuple) of - %% avoid logging the token - {refused, {error, {invalid_token, error, _Err, _Stacktrace}}} -> - {refused, "Authentication using an OAuth 2/JWT token failed: provided token is invalid"}; - {refused, Err} -> - {refused, rabbit_misc:format("Authentication using an OAuth 2/JWT token failed: ~tp", [Err])}; - {ok, DecodedToken} -> - CurToken = AuthUser#auth_user.impl, - case ensure_same_username( - ResourceServer#resource_server.preferred_username_claims, - CurToken(), DecodedToken) of - ok -> - Tags = tags_from(DecodedToken), - {ok, AuthUser#auth_user{tags = Tags, - impl = fun() -> DecodedToken end}}; - {error, mismatch_username_after_token_refresh} -> - {refused, - "Not allowed to change username on refreshed token"} - end + TokenResult = case oauth2_client:is_jwt_token(NewToken) of + true -> {ok, NewToken}; + false -> + case oauth2_client:introspect_token(NewToken) of + {ok, Tk1} -> + ?LOG_DEBUG("Successfully (update_state) introspected token : ~p", [Tk1]), + {ok, Tk1}; + {error, Err1} -> + ?LOG_ERROR("Failed to introspected token due to ~p", [Err1]), + {error, Err1} end + end, + case TokenResult of + {ok, Token} -> + case resolve_resource_server(Token) of + {error, _} = Err0 -> Err0; + {ResourceServer, _} = Tuple -> + case check_token(Token, Tuple) of + %% avoid logging the token + {refused, {error, {invalid_token, error, _Err, _Stacktrace}}} -> + {refused, "Authentication using an OAuth 2/JWT token failed: provided token is invalid"}; + {refused, Err} -> + {refused, rabbit_misc:format("Authentication using an OAuth 2/JWT token failed: ~tp", [Err])}; + {ok, DecodedToken} -> + CurToken = AuthUser#auth_user.impl, + case ensure_same_username( + ResourceServer#resource_server.preferred_username_claims, + CurToken(), DecodedToken) of + ok -> + Tags = tags_from(DecodedToken), + ?LOG_DEBUG("Updated credentials with new token: ~p and tags: ~p", + [DecodedToken, Tags]), + {ok, AuthUser#auth_user{tags = Tags, + impl = fun() -> DecodedToken end}}; + {error, mismatch_username_after_token_refresh} -> + {refused, + "Not allowed to change username on refreshed token"} + end + end + end; + {error, _} -> {refused, "Unable to introspect token"} end. expiry_timestamp(#auth_user{impl = DecodedTokenFun}) -> @@ -152,24 +170,40 @@ expiry_timestamp(#auth_user{impl = DecodedTokenFun}) -> authenticate(_, AuthProps0) -> AuthProps = to_map(AuthProps0), - Token = token_from_context(AuthProps), - case resolve_resource_server(Token) of - {error, _} = Err0 -> - {refused, "Authentication using OAuth 2/JWT token failed: ~tp", [Err0]}; - {ResourceServer, _} = Tuple -> - case check_token(Token, Tuple) of - {refused, {error, {invalid_token, error, _Err, _Stacktrace}}} -> - {refused, "Authentication using an OAuth 2/JWT token failed: provided token is invalid", []}; - {refused, Err} -> - {refused, "Authentication using an OAuth 2/JWT token failed: ~tp", [Err]}; - {ok, DecodedToken} -> - case with_decoded_token(DecodedToken, fun(In) -> auth_user_from_token(In, ResourceServer) end) of - {error, Err} -> + Token0 = token_from_context(AuthProps), + TokenResult = case oauth2_client:is_jwt_token(Token0) of + true -> {ok, Token0}; + false -> + case oauth2_client:introspect_token(Token0) of + {ok, Tk1} -> + ?LOG_DEBUG("Successfully (authenticate) introspected token : ~p", [Tk1]), + {ok, Tk1}; + {error, Err1} -> + ?LOG_ERROR("Failed to introspected token due to ~p", [Err1]), + {error, Err1} + end + end, + case TokenResult of + {ok, Token} -> + case resolve_resource_server(Token) of + {error, _} = Err0 -> + {refused, "Authentication using OAuth 2/JWT token failed: ~tp", [Err0]}; + {ResourceServer, _} = Tuple -> + case check_token(Token, Tuple) of + {refused, {error, {invalid_token, error, _Err, _Stacktrace}}} -> + {refused, "Authentication using an OAuth 2/JWT token failed: provided token is invalid", []}; + {refused, Err} -> {refused, "Authentication using an OAuth 2/JWT token failed: ~tp", [Err]}; - Else -> - Else + {ok, DecodedToken} -> + case with_decoded_token(DecodedToken, fun(In) -> auth_user_from_token(In, ResourceServer) end) of + {error, Err} -> + {refused, "Authentication using an OAuth 2/JWT token failed: ~tp", [Err]}; + Else -> + Else + end end - end + end; + {error, Error} -> {refused, "Unable to introspect token: ~p", [Error]} end. -spec with_decoded_token(Token, Fun) -> Result @@ -205,6 +239,7 @@ ensure_same_username(PreferredUsernameClaims, CurrentDecodedToken, NewDecodedTok _ -> {error, mismatch_username_after_token_refresh} end. + validate_token_expiry(#{<<"exp">> := Exp}) when is_integer(Exp) -> Now = os:system_time(seconds), case Exp =< Now of @@ -218,8 +253,11 @@ validate_token_expiry(#{}) -> ok. {'error', term() } | {'refused', 'signature_invalid' | {'error', term()} | {'invalid_aud', term()}}. -check_token(DecodedToken, _) when is_map(DecodedToken) -> - {ok, DecodedToken}; +check_token(DecodedToken, {ResourceServer, _}) when is_map(DecodedToken) -> + case maps:is_key(?ACTIVE_FIELD, DecodedToken) of + false -> {ok, DecodedToken}; + true -> {ok, normalize_token_scope(ResourceServer, DecodedToken)} + end; check_token(Token, {ResourceServer, InternalOAuthProvider}) -> case decode_and_verify(Token, ResourceServer, InternalOAuthProvider) of @@ -240,7 +278,6 @@ extract_scopes_from_scope_claim(Payload) -> -spec normalize_token_scope( ResourceServer :: resource_server(), DecodedToken :: decoded_jwt_token()) -> map(). normalize_token_scope(ResourceServer, Payload) -> - filter_duplicates( filter_matching_scope_prefix(ResourceServer, extract_scopes_from_rich_auth_request(ResourceServer, @@ -248,7 +285,7 @@ normalize_token_scope(ResourceServer, Payload) -> extract_scopes_from_additional_scopes_key(ResourceServer, extract_scopes_from_requesting_party_token(ResourceServer, extract_scopes_from_scope_claim(Payload))))))). - + filter_duplicates(#{?SCOPE_JWT_FIELD := Scopes} = Payload) -> set_scope(lists:usort(Scopes), Payload); filter_duplicates(Payload) -> Payload. @@ -475,5 +512,6 @@ resolve_scope_var(Elem, Token, Vhost) -> -spec tags_from(decoded_jwt_token()) -> list(atom()). tags_from(DecodedToken) -> Scopes = maps:get(?SCOPE_JWT_FIELD, DecodedToken, []), + ?LOG_DEBUG("tags_from Scopes : ~p", [Scopes]), TagScopes = filter_matching_scope_prefix_and_drop_it(Scopes, ?TAG_SCOPE_PREFIX), lists:usort(lists:map(fun rabbit_data_coercion:to_atom/1, TagScopes)). diff --git a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_provider.erl b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_provider.erl index 0ac4c7c46dda..92f7bf38abc6 100644 --- a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_provider.erl +++ b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_provider.erl @@ -29,7 +29,6 @@ get_internal_oauth_provider(OAuthProviderId) -> algorithms = get_algorithms(OAuthProviderId) }. - %% %% Signing Key storage: %% @@ -147,9 +146,17 @@ get_signing_keys(OauthProviderId) -> end. get_signing_key(KeyId) -> - maps:get(KeyId, get_signing_keys(root), undefined). + get_signing_key(KeyId, root). + get_signing_key(KeyId, OAuthProviderId) -> - maps:get(KeyId, get_signing_keys(OAuthProviderId), undefined). + case maps:get(KeyId, get_signing_keys(OAuthProviderId), undefined) of + undefined -> + case oauth2_client:get_opaque_token_signing_key(KeyId) of + {ok, SK} -> SK; + {error, _} -> undefined + end; + V -> V + end. -spec get_default_key(oauth_provider_id()) -> binary() | undefined. get_default_key(root) -> diff --git a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_resource_server.erl b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_resource_server.erl index 56d97d12822c..0269e5641831 100644 --- a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_resource_server.erl +++ b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_resource_server.erl @@ -8,6 +8,7 @@ -module(rabbit_oauth2_resource_server). -include("oauth2.hrl"). +-include_lib("kernel/include/logger.hrl"). -export([ resolve_resource_server_from_audience/1, @@ -166,12 +167,17 @@ find_audience(Audience, ResourceIdList) when is_binary(Audience) -> AudList = binary:split(Audience, <<" ">>, [global, trim_all]), find_audience(AudList, ResourceIdList); find_audience(AudList, ResourceIdList) when is_list(AudList) -> - case intersection(AudList, ResourceIdList) of + case intersection(normalize_to_binary_list(AudList), ResourceIdList) of [One] -> {ok, One}; [_One|_Tail] -> {error, aud_matched_many_resource_servers_only_one_allowed}; [] -> {error, no_matching_aud_found} end. +-spec normalize_to_binary_list(binary() | string() | [binary()]) -> [binary()]. +normalize_to_binary_list([H | _] = List) when is_binary(H) -> List; +normalize_to_binary_list(Input) when is_list(Input) -> + [unicode:characters_to_binary(Part) || Part <- string:tokens(Input, " ")]. + -spec translate_error_if_any( {ok, resource_server()} | {error, not_found} | diff --git a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_schema.erl b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_schema.erl index 13cf6b38de03..4d819ec0acc7 100644 --- a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_schema.erl +++ b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_schema.erl @@ -12,6 +12,8 @@ -define(RESOURCE_SERVERS, "resource_servers"). -define(OAUTH_PROVIDERS, "oauth_providers"). -define(SIGNING_KEYS, "signing_keys"). +-define(OPAQUE_TOKEN_SIGNING_KEY, "opaque_token_signing_key"). + -define(AUTH_OAUTH2_SCOPE_ALIASES, ?AUTH_OAUTH2 ++ "." ++ ?SCOPE_ALIASES). -define(AUTH_OAUTH2_RESOURCE_SERVERS, ?AUTH_OAUTH2 ++ "." ++ ?RESOURCE_SERVERS). -define(AUTH_OAUTH2_OAUTH_PROVIDERS, ?AUTH_OAUTH2 ++ "." ++ ?OAUTH_PROVIDERS). @@ -25,7 +27,8 @@ translate_resource_servers/1, translate_signing_keys/1, translate_endpoint_params/2, - translate_scope_aliases/1 + translate_scope_aliases/1, + translate_opaque_token_signing_key/1 ]). resource_servers_key_synonym(Key) -> maps:get(Key, ?RESOURCE_SERVERS_SYNONYMS, Key). @@ -193,6 +196,13 @@ translate_endpoint_params(Variable, Conf) -> [{list_to_binary(Param), list_to_binary(V)} || {["auth_oauth2", _, Param], V} <- Params0]. +-spec translate_opaque_token_signing_key([{list(), binary()}]) -> + [{atom(), binary()}]. +translate_opaque_token_signing_key(Conf) -> + Params0 = cuttlefish_variable:filter_by_prefix("auth_oauth2.opaque_token_signing_key", + Conf), + extract_opaque_token_signing_key_properties(Params0). + validator_file_exists(Attr, Filename) -> case file:read_file(Filename) of {ok, _} -> @@ -232,27 +242,51 @@ merge_list_of_maps(ListOfMaps) -> extract_oauth_providers_properties(Settings) -> KeyFun = fun extract_key_as_binary/1, ValueFun = fun extract_value/1, + MapValueFun = fun(V) -> + case V of + L when is_list(L) -> list_to_binary(L); + B when is_binary(B) -> B; + _ -> V + end end, OAuthProviders = [{Name, mapOauthProviderProperty( { list_to_atom(Key), - list_to_binary(V)}) + MapValueFun(V)}) } || {[?AUTH_OAUTH2, ?OAUTH_PROVIDERS, Name, Key], V} <- Settings ], maps:groups_from_list(KeyFun, ValueFun, OAuthProviders). +extract_opaque_token_signing_key_properties(Settings) -> + MapValueFun = fun(K, V) -> + case {K, V} of + {"type", A} when is_atom(A) -> A; + {"type", _} -> list_to_atom(V); + {"id", L} when is_list(L) -> list_to_binary(L); + {"id", B} when is_binary(B) -> B; + {"key", L} when is_list(L) -> list_to_binary(L); + {"key", B} when is_binary(B) -> B + end end, + + Translation = [{ + list_to_atom(Key), + MapValueFun(Key, V) + } || {[?AUTH_OAUTH2, ?OPAQUE_TOKEN_SIGNING_KEY, Key], V} <- Settings ], + Translation. extract_resource_server_properties(Settings) -> KeyFun = fun extract_key_as_binary/1, ValueFun = fun extract_value/1, - OAuthProviders = [{Name, {list_to_atom(resource_servers_key_synonym(Key)), list_to_binary(V)}} + ResourceServers = [{Name, {list_to_atom(resource_servers_key_synonym(Key)), list_to_binary(V)}} || {[?AUTH_OAUTH2, ?RESOURCE_SERVERS, Name, Key], V} <- Settings ], - maps:groups_from_list(KeyFun, ValueFun, OAuthProviders). + maps:groups_from_list(KeyFun, ValueFun, ResourceServers). + mapOauthProviderProperty({Key, Value}) -> {Key, case Key of issuer -> validator_https_uri(Key, Value); token_endpoint -> validator_https_uri(Key, Value); + introspection_endpoint -> validator_https_uri(Key, Value); jwks_uri -> validator_https_uri(Key, Value); end_session_endpoint -> validator_https_uri(Key, Value); authorization_endpoint -> validator_https_uri(Key, Value); diff --git a/deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt.erl b/deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt.erl index c293b9c347dd..216a6c10780d 100644 --- a/deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt.erl +++ b/deps/rabbitmq_auth_backend_oauth2/src/uaa_jwt.erl @@ -146,7 +146,10 @@ get_jwk(KeyId, InternalOAuthProvider, AllowUpdateJwks) -> pem_file -> uaa_jwt_jwk:from_pem_file(Value); map -> uaa_jwt_jwk:make_jwk(Value); _ -> {error, unknown_signing_key_type} - end + end; + SK -> + ?LOG_DEBUG("Opaque token Signing key ~p found", [KeyId]), + {ok, SK#signing_key.key} end. verify_signing_key(Type, Value) -> @@ -189,3 +192,4 @@ sub(DecodedToken) -> -spec sub(map(), any()) -> binary() | undefined. sub(DecodedToken, Default) -> maps:get(<<"sub">>, DecodedToken, Default). + diff --git a/deps/rabbitmq_auth_backend_oauth2/test/config_schema_SUITE_data/rabbitmq_auth_backend_oauth2.snippets b/deps/rabbitmq_auth_backend_oauth2/test/config_schema_SUITE_data/rabbitmq_auth_backend_oauth2.snippets index 4db415c113a3..38bf100578d9 100644 --- a/deps/rabbitmq_auth_backend_oauth2/test/config_schema_SUITE_data/rabbitmq_auth_backend_oauth2.snippets +++ b/deps/rabbitmq_auth_backend_oauth2/test/config_schema_SUITE_data/rabbitmq_auth_backend_oauth2.snippets @@ -31,7 +31,7 @@ {resource_server_type,<<"new_resource_server_type">>}, {extra_scopes_source, <<"my_custom_scope_key">>}, {preferred_username_claims, [<<"user_name">>, <<"username">>, <<"email">>]}, - {verify_aud, true}, + {verify_aud, true}, {issuer, "https://my-jwt-issuer"}, {discovery_endpoint_path, "/.well-known/openid-configuration"}, {discovery_endpoint_params, [ @@ -96,14 +96,14 @@ {jwks_uri, "https://my-jwt-issuer/jwks.json"}, {resource_servers, #{ + <<"rabbitmq-customers">> => [ + {extra_scopes_source, <<"roles">>}, + {id, <<"rabbitmq-customers">>} + ], <<"rabbitmq-operations">> => [ {scope_prefix, <<"api://">>}, {id, <<"rabbitmq-operations">>} - ], - <<"rabbitmq-customers">> => [ - {extra_scopes_source, <<"roles">>}, - {id, <<"rabbitmq-customers">>} - ] + ] } }, {key_config, [ @@ -326,5 +326,60 @@ {extra_scopes_source, <<"roles realm.roles">> } ]} ], [] + }, + {token_introspection, + "auth_oauth2.resource_server_id = new_resource_server_id + auth_oauth2.introspection_endpoint = https://introspect + auth_oauth2.introspection_client_auth_method = basic + auth_oauth2.introspection_client_id = rabbit + auth_oauth2.introspection_client_secret = rabbit_secret", + [ + {rabbitmq_auth_backend_oauth2, [ + {introspection_client_auth_method, basic }, + {introspection_client_id, <<"rabbit">> }, + {introspection_client_secret, <<"rabbit_secret">> }, + {introspection_endpoint, "https://introspect"}, + {resource_server_id, <<"new_resource_server_id">>} + ] + } + ], [] + }, + {token_introspection_via_oauth_providers, + "auth_oauth2.resource_server_id = new_resource_server_id + auth_oauth2.oauth_providers.p.introspection_endpoint = https://introspect + auth_oauth2.oauth_providers.p.introspection_client_id = rabbit + auth_oauth2.oauth_providers.p.introspection_client_auth_method = basic + auth_oauth2.oauth_providers.p.introspection_client_secret = rabbit_secret", + [ + {rabbitmq_auth_backend_oauth2, [ + {resource_server_id, <<"new_resource_server_id">>}, + {oauth_providers, #{ + <<"p">> => [ + {introspection_client_secret, <<"rabbit_secret">>}, + {introspection_client_auth_method, basic}, + {introspection_client_id, <<"rabbit">>}, + {introspection_endpoint, "https://introspect"} + ] + }} + ]} + ], [] + }, + {opaque_token_signing_key, + "auth_oauth2.resource_server_id = new_resource_server_id + auth_oauth2.opaque_token_signing_key.id = opaque-id + auth_oauth2.opaque_token_signing_key.type = hs256 + auth_oauth2.opaque_token_signing_key.key = opaque-signing-key", + [ + {rabbitmq_auth_backend_oauth2, [ + {resource_server_id, <<"new_resource_server_id">>}, + {opaque_token_signing_key, [ + {id, <<"opaque-id">>}, + {type, hs256}, + {key, <<"opaque-signing-key">>} + ] + } + ]} + ], [] } + ]. diff --git a/deps/rabbitmq_auth_backend_oauth2/test/introspect_http_handler.erl b/deps/rabbitmq_auth_backend_oauth2/test/introspect_http_handler.erl new file mode 100644 index 000000000000..6655350bbf41 --- /dev/null +++ b/deps/rabbitmq_auth_backend_oauth2/test/introspect_http_handler.erl @@ -0,0 +1,47 @@ +-module(introspect_http_handler). +-behavior(cowboy_handler). + +-export([init/2, terminate/3]). + +init(Req, State) -> + ct:log("introspect_http_handler init : ~p", [Req]), + case cowboy_req:read_urlencoded_body(Req) of + {ok, KeyValues, _Req} -> + ct:log("introspect_http_handler responding with active token: ~p", [KeyValues]), + case proplists:get_value(<<"token">>, KeyValues) of + <<"401">> -> + {ok, cowboy_req:reply(401, #{}, [], Req), State}; + <<"active">> -> + Body = rabbit_json:encode([ + {"active", true}, + {"sub", <<"test_case">>}, + {"exp", os:system_time(seconds) + 30}, + {"aud", <<"rabbitmq">>}, + {"scope", <<"rabbitmq.configure:*/* rabbitmq.write:*/* rabbitmq.read:*/*">>}]), + {ok, cowboy_req:reply(200, #{<<"content-type">> => <<"application/json">>}, + Body, Req), State}; + <<"active-2">> -> + Body = rabbit_json:encode([ + {"active", true}, + {"sub", <<"test_case">>}, + {"exp", os:system_time(seconds) + 30}, + {"aud", <<"rabbitmq">>}, + {"scope", <<"rabbitmq.write:*/* rabbitmq.read:*/*">>}]), + {ok, cowboy_req:reply(200, #{<<"content-type">> => <<"application/json">>}, + Body, Req), State}; + <<"inactive">> -> + Body = rabbit_json:encode([ + {"active", false}, + {"sub", <<"test_case">>}, + {"exp", os:system_time(seconds) + 30}, + {"scope", <<"rabbitmq.configure:*/* rabbitmq.write:*/* rabbitmq.read:*/*">>}]), + {ok, cowboy_req:reply(200, #{<<"content-type">> => <<"application/json">>}, + Body, Req), State} + end; + Other -> + ct:log("introspect_http_handler responding with 401 : ~p", [Other]), + {ok, cowboy_req:reply(401, #{}, [], Req), State} + end. + +terminate(_Reason, _Req, _State) -> + ok. diff --git a/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_provider_SUITE.erl b/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_provider_SUITE.erl index 7e81cda4f773..5bd0bc59e9b6 100644 --- a/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_provider_SUITE.erl +++ b/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_provider_SUITE.erl @@ -479,7 +479,7 @@ start_https_oauth_server(Port, CertsDir, Expectations) when is_list(Expectations {'_', [{Path, oauth2_http_mock, Expected} || #{request := #{path := Path}} = Expected <- Expectations ]} ]), - {ok, Pid} = cowboy:start_tls( + {ok, _Pid} = cowboy:start_tls( mock_http_auth_listener, [{port, Port}, {certfile, filename:join([CertsDir, "server", "cert.pem"])}, diff --git a/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_resource_server_SUITE.erl b/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_resource_server_SUITE.erl index 064edb34944d..f1e52b2efe4b 100644 --- a/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_resource_server_SUITE.erl +++ b/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_resource_server_SUITE.erl @@ -57,7 +57,7 @@ groups() -> [ {with_rabbitmq1_verify_aud_false, [], [ resolve_resource_server_for_none_audience_returns_error ]} - ]}, + ]}, verify_rabbitmq1_server_configuration, {verify_configuration_inheritance_with_rabbitmq2, [], verify_configuration_inheritance_with_rabbitmq2()}, diff --git a/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_schema_SUITE.erl b/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_schema_SUITE.erl index 1fdd30ab2b64..5a3eff8339a7 100644 --- a/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_schema_SUITE.erl +++ b/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_schema_SUITE.erl @@ -41,7 +41,8 @@ all() -> test_without_oauth_providers_with_endpoint_params, test_scope_aliases_configured_as_list_of_properties, test_scope_aliases_configured_as_map, - test_scope_aliases_configured_as_list_of_missing_properties + test_scope_aliases_configured_as_list_of_missing_properties, + test_opaque_token_signing_key ]. @@ -326,6 +327,21 @@ test_scope_aliases_configured_as_map(_) -> <<"developer">> := [<<"rabbitmq.tag:management">>, <<"rabbitmq.read:*/*">>] } = rabbit_oauth2_schema:translate_scope_aliases(CuttlefishConf). +test_opaque_token_signing_key(_) -> + CuttlefishConf = [ + {["auth_oauth2","opaque_token_signing_key","id"], + "key-id"}, + {["auth_oauth2","opaque_token_signing_key","type"], + "hs256"}, + {["auth_oauth2","opaque_token_signing_key","key"], + "signing-key"} + ], + [ + {id, <<"key-id">>}, + {type, hs256}, + {key, <<"signing-key">>} + ] = rabbit_oauth2_schema:translate_opaque_token_signing_key(CuttlefishConf). + cert_filename(Conf) -> string:concat(?config(data_dir, Conf), "certs/cert.pem"). diff --git a/deps/rabbitmq_auth_backend_oauth2/test/system_SUITE.erl b/deps/rabbitmq_auth_backend_oauth2/test/system_SUITE.erl index 65e10bb87e38..d5e31c2433b3 100644 --- a/deps/rabbitmq_auth_backend_oauth2/test/system_SUITE.erl +++ b/deps/rabbitmq_auth_backend_oauth2/test/system_SUITE.erl @@ -26,7 +26,8 @@ all() -> {group, token_refresh}, {group, extra_scopes_source}, {group, scope_aliases}, - {group, rich_authorization_requests} + {group, rich_authorization_requests}, + {group, with_introspection_endpoint} ]. groups() -> @@ -83,6 +84,13 @@ groups() -> amqp_token_refresh_expire, amqp_token_refresh_vhost_permission, amqp_token_refresh_revoked_permissions + ]}, + {with_introspection_endpoint, [], [ + test_successful_connection_with_valid_opaque_token, + test_unsuccessful_connection_with_invalid_opaque_token, + test_successful_opaque_token_refresh, + test_successful_opaque_token_refresh_with_more_restrictive_token, + test_unsuccessful_opaque_token_refresh_with_inactive_token ]} ]. @@ -113,6 +121,27 @@ end_per_suite(Config) -> init_per_group(amqp, Config) -> {ok, _} = application:ensure_all_started(rabbitmq_amqp_client), Config; +init_per_group(with_introspection_endpoint, Config) -> + {ok, _} = application:ensure_all_started(ssl), + {ok, _} = application:ensure_all_started(cowboy), + + PortBase = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_ports_base), + Port = PortBase + 100, + AuthorizationServerURL = uri_string:normalize(#{ + scheme => "https",port => Port,path => "/introspect",host => "localhost"}), + + CertsDir = ?config(rmq_certsdir, Config), + Endpoints = [ {"/introspect", introspect_http_handler, []}], + Dispatch = cowboy_router:compile([{'_', Endpoints}]), + {ok, _} = cowboy:start_tls(introspection_http_listener, + [{port, Port}, + {certfile, filename:join([CertsDir, "server", "cert.pem"])}, + {keyfile, filename:join([CertsDir, "server", "key.pem"])}], + #{env => #{dispatch => Dispatch}}), + + [ {authorization_server_url, AuthorizationServerURL}, + {authorization_server_ca_cert, filename:join([CertsDir, "testca", "cacert.pem"])} | Config]; + init_per_group(_Group, Config) -> %% The broker is managed by {init,end}_per_testcase(). lists:foreach(fun(Value) -> @@ -123,6 +152,12 @@ init_per_group(_Group, Config) -> end_per_group(amqp, Config) -> Config; + +end_per_group(with_introspection_endpoint, Config) -> + ok = cowboy:stop_listener(introspection_http_listener), + inets:stop(), + Config; + end_per_group(_Group, Config) -> %% The broker is managed by {init,end}_per_testcase(). lists:foreach(fun(Value) -> @@ -131,6 +166,33 @@ end_per_group(_Group, Config) -> [<<"vhost1">>, <<"vhost2">>, <<"vhost3">>, <<"vhost4">>]), Config. +setup_introspection_configuration(Config) -> + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, introspection_endpoint, + ?config(authorization_server_url, Config)]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, introspection_client_id, "some-id"]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, introspection_client_secret, "some-secret"]), + CaCertFile = ?config(authorization_server_ca_cert, Config), + + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, key_config, [{cacertfile, CaCertFile}]]), + + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, opaque_token_signing_key, + [{id, <<"rabbit_key">>}, {type, hs256}, {key, <<"some-key">>}]]), + Config. + +teardown_introspection_configuration(Config) -> + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, introspection_endpoint]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, introspection_client_id]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, introspection_client_secret]), + Config. + %% %% Per-case setup %% @@ -234,22 +296,29 @@ init_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection Config; init_per_testcase(multiple_resource_server_ids, Config) -> - ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, - [rabbitmq_auth_backend_oauth2, scope_prefix, <<"rmq.">> ]), - ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, - [rabbitmq_auth_backend_oauth2, resource_servers, #{ - <<"prod">> => [ ], - <<"dev">> => [ ] - }]), - rabbit_ct_helpers:testcase_started(Config, multiple_resource_server_ids), - Config; + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, scope_prefix, <<"rmq.">> ]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, resource_servers, #{ + <<"prod">> => [ ], + <<"dev">> => [ ] + }]), + rabbit_ct_helpers:testcase_started(Config, multiple_resource_server_ids), + Config; + +init_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection_with_valid_opaque_token orelse + Testcase =:= test_successful_opaque_token_refresh orelse + Testcase =:= test_successful_opaque_token_refresh_with_more_restrictive_token orelse + Testcase =:= test_unsuccessful_opaque_token_refresh_with_inactive_token -> + rabbit_ct_broker_helpers:add_vhost(Config, <<"vhost1">>), + rabbit_ct_helpers:testcase_started( + setup_introspection_configuration(Config), Testcase); init_per_testcase(Testcase, Config) -> rabbit_ct_helpers:testcase_started(Config, Testcase), Config. - %% %% Per-case Teardown %% @@ -263,52 +332,58 @@ end_per_testcase(Testcase, Config) when Testcase =:= test_failed_token_refresh_c end_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection_with_complex_claim_as_a_map orelse Testcase =:= test_successful_connection_with_complex_claim_as_a_list orelse Testcase =:= test_successful_connection_with_complex_claim_as_a_binary -> - rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost1">>), - ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, - [rabbitmq_auth_backend_oauth2, extra_scopes_source]), - rabbit_ct_helpers:testcase_finished(Config, Testcase), - Config; + rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost1">>), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, extra_scopes_source]), + rabbit_ct_helpers:testcase_finished(Config, Testcase), + Config; end_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection_with_with_single_scope_alias_in_extra_scopes_source -> - rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost1">>), - ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, - [rabbitmq_auth_backend_oauth2, scope_aliases]), - ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, - [rabbitmq_auth_backend_oauth2, extra_scopes_source]), - rabbit_ct_helpers:testcase_finished(Config, Testcase), - Config; + rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost1">>), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, scope_aliases]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, extra_scopes_source]), + rabbit_ct_helpers:testcase_finished(Config, Testcase), + Config; end_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection_with_with_multiple_scope_aliases_in_extra_scopes_source -> - rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost4">>), - ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, - [rabbitmq_auth_backend_oauth2, scope_aliases]), - ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, - [rabbitmq_auth_backend_oauth2, extra_scopes_source]), - rabbit_ct_helpers:testcase_finished(Config, Testcase), - Config; + rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost4">>), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, scope_aliases]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, extra_scopes_source]), + rabbit_ct_helpers:testcase_finished(Config, Testcase), + Config; end_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection_with_scope_alias_in_scope_field_case1 orelse Testcase =:= test_successful_connection_with_scope_alias_in_scope_field_case2 -> - rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost2">>), - ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, - [rabbitmq_auth_backend_oauth2, scope_aliases]), - rabbit_ct_helpers:testcase_finished(Config, Testcase), - Config; + rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost2">>), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, scope_aliases]), + rabbit_ct_helpers:testcase_finished(Config, Testcase), + Config; end_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection_with_scope_alias_in_scope_field_case3 -> - rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost3">>), - ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, - [rabbitmq_auth_backend_oauth2, scope_aliases]), - rabbit_ct_helpers:testcase_finished(Config, Testcase), - Config; + rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost3">>), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, scope_aliases]), + rabbit_ct_helpers:testcase_finished(Config, Testcase), + Config; end_per_testcase(multiple_resource_server_ids, Config) -> - rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, - [rabbitmq_auth_backend_oauth2, scope_prefix ]), - rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, - [rabbitmq_auth_backend_oauth2, resource_servers ]), - rabbit_ct_helpers:testcase_started(Config, multiple_resource_server_ids), - Config; + rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, scope_prefix ]), + rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, resource_servers ]), + rabbit_ct_helpers:testcase_started(Config, multiple_resource_server_ids), + Config; + +end_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection_with_valid_opaque_token orelse + Testcase =:= test_successful_opaque_token_refresh orelse + Testcase =:= test_successful_opaque_token_refresh_with_more_restrictive_token orelse + Testcase =:= test_unsuccessful_opaque_token_refresh_with_inactive_token -> + teardown_introspection_configuration(Config); end_per_testcase(Testcase, Config) -> rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost1">>), @@ -448,6 +523,64 @@ test_successful_connection_without_verify_aud(Config) -> amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), close_connection_and_channel(Conn, Ch). +test_successful_connection_with_valid_opaque_token(Config) -> + Conn = open_unmanaged_connection(Config, 0, <<"username">>, <<"active">>), + {ok, Ch} = amqp_connection:open_channel(Conn), + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + close_connection_and_channel(Conn, Ch). + +test_unsuccessful_connection_with_invalid_opaque_token(Config) -> + {error, Error} = open_unmanaged_connection(Config, 0, <<"username">>, <<"inactive">>), + ct:log("Error : ~p", [Error]). + +test_successful_opaque_token_refresh(Config) -> + Conn = open_unmanaged_connection(Config, 0, <<"vhost1">>, <<"username">>, <<"active">>), + {ok, Ch} = amqp_connection:open_channel(Conn), + + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + + ?assertEqual(ok, amqp_connection:update_secret(Conn, <<"active">>, <<"token refresh">>)), + + {ok, Ch2} = amqp_connection:open_channel(Conn), + + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch2, #'queue.declare'{exclusive = true}), + + close_connection_and_channel(Conn, Ch). + +test_successful_opaque_token_refresh_with_more_restrictive_token(Config) -> + Conn = open_unmanaged_connection(Config, 0, <<"vhost1">>, <<"username">>, <<"active">>), + {ok, Ch} = amqp_connection:open_channel(Conn), + + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + + ?assertEqual(ok, amqp_connection:update_secret(Conn, <<"active-2">>, <<"token refresh">>)), + + {ok, Ch2} = amqp_connection:open_channel(Conn), + + ?assertExit({{shutdown, {server_initiated_close, 403, _}}, _}, + amqp_channel:call(Ch2, #'queue.declare'{queue = <<"a.q">>, exclusive = true})), + + catch close_connection(Conn). + +test_unsuccessful_opaque_token_refresh_with_inactive_token(Config) -> + Conn = open_unmanaged_connection(Config, 0, <<"vhost1">>, <<"username">>, <<"active">>), + {ok, Ch} = amqp_connection:open_channel(Conn), + + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + + amqp_connection:update_secret(Conn, <<"inactive">>, <<"token refresh">>), + + ?assertExit({{shutdown, {connection_closing, {server_initiated_close, 530, _}}}, _}, + amqp_connection:open_channel(Conn)), + + catch close_connection(Conn). + + mqtt(Config) -> Topic = <<"test/topic">>, Payload = <<"mqtt-test-message">>, @@ -955,12 +1088,17 @@ test_failed_token_refresh_case1(Config) -> catch close_connection(Conn). refreshed_token_cannot_change_username(Config) -> - {_, Token} = generate_valid_token_with_sub(Config, <<"username">>), - Conn = open_unmanaged_connection(Config, 0, <<"vhost4">>, <<"username">>, Token), + {_, Token} = generate_valid_token_with_sub(Config, <<"username3">>), + Conn = open_unmanaged_connection(Config, 0, <<"vhost1">>, <<"username3">>, Token), {_, RefreshedToken} = generate_valid_token_with_sub(Config, <<"username2">>), %% the error is communicated asynchronously via a connection-level error - ?assertException(exit, {{nodedown,not_allowed},_}, amqp_connection:update_secret(Conn, RefreshedToken, <<"token refresh">>)). + amqp_connection:update_secret(Conn, RefreshedToken, <<"should fail token refresh">>), + + ?assertExit({{shutdown, {connection_closing, {server_initiated_close, 530, _}}}, _}, + amqp_connection:open_channel(Conn)), + + catch close_connection(Conn). test_failed_token_refresh_case2(Config) -> @@ -1062,3 +1200,4 @@ flush(Prefix) -> after 1 -> ok end. + diff --git a/deps/rabbitmq_management/priv/schema/rabbitmq_management.schema b/deps/rabbitmq_management/priv/schema/rabbitmq_management.schema index 1a1b837b0486..ded76247bcb3 100644 --- a/deps/rabbitmq_management/priv/schema/rabbitmq_management.schema +++ b/deps/rabbitmq_management/priv/schema/rabbitmq_management.schema @@ -470,7 +470,6 @@ end}. {mapping, "management.oauth_provider_url", "rabbitmq_management.oauth_provider_url", [{datatype, string}]}. - %% Your client application's identifier as registered with the OIDC/OAuth2 {mapping, "management.oauth_client_id", "rabbitmq_management.oauth_client_id", [{datatype, string}]}. diff --git a/deps/rabbitmq_management/priv/www/js/main.js b/deps/rabbitmq_management/priv/www/js/main.js index 7e910978ed12..fec0dffbb947 100644 --- a/deps/rabbitmq_management/priv/www/js/main.js +++ b/deps/rabbitmq_management/priv/www/js/main.js @@ -1389,7 +1389,8 @@ function sync_req(type, params0, path_template, options) { var req = xmlHttpRequest(); req.open(type, 'api' + path, false); req.setRequestHeader('content-type', 'application/json'); - req.setRequestHeader('authorization', authorization_header()); + let authorization = authorization_header() + req.setRequestHeader('authorization', authorization); if (options != undefined || options != null) { if (options.headers != undefined || options.headers != null) { diff --git a/deps/rabbitmq_management/priv/www/js/oidc-oauth/helper.js b/deps/rabbitmq_management/priv/www/js/oidc-oauth/helper.js index be84377e22d6..697229ebc0de 100644 --- a/deps/rabbitmq_management/priv/www/js/oidc-oauth/helper.js +++ b/deps/rabbitmq_management/priv/www/js/oidc-oauth/helper.js @@ -124,6 +124,11 @@ export function oauth_initiate(oauth) { if (!status.loggedIn) { clear_auth(); } else { + if (!is_jwt_token(status.user.access_token)) { + console.log("Introspect opaque token ...") + set_token_auth(introspect_token(status.user.access_token)) + console.log("Introspected token") + } oauth.logged_in = true; oauth.expiryDate = new Date(status.user.expires_at * 1000); // it is epoch in seconds let current = new Date(); @@ -190,6 +195,12 @@ function oauth_initialize_user_manager(resource_server) { }); mgr.events.addUserLoaded(function(user) { set_token_auth(user.access_token) +/* if (!is_jwt_token(user.access_token)) { + console.log("addUserLoaded: Detected opaque token. Introspecting it ...") + set_token_auth(introspect_token()) + console.log("Introspected token") + } +*/ }); } @@ -280,13 +291,26 @@ function oauth_redirectToLogin(error) { } export function oauth_completeLogin() { mgr.signinRedirectCallback().then(function(user) { - set_token_auth(user.access_token); - oauth_redirectToHome(); + set_token_auth(user.access_token) + oauth_redirectToHome() }).catch(function(err) { _management_logger.error(err) oauth_redirectToLogin(err) }); } +function introspect_token() { + let jwt = JSON.parse(sync_post({}, '/auth/introspect').responseText) + console.log("jwt token : " + jwt) + return jwt.token +} + +function is_jwt_token(token) { + if (token != null) { + return token.split(".").length == 3 + }else { + return false + } +} export function oauth_initiateLogout() { if (oauth.sp_initiated) { diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_dispatcher.erl b/deps/rabbitmq_management/src/rabbit_mgmt_dispatcher.erl index 9f939558563a..61268c173134 100644 --- a/deps/rabbitmq_management/src/rabbit_mgmt_dispatcher.erl +++ b/deps/rabbitmq_management/src/rabbit_mgmt_dispatcher.erl @@ -214,6 +214,7 @@ dispatcher() -> {"/reset/:node", rabbit_mgmt_wm_reset, []}, {"/rebalance/queues", rabbit_mgmt_wm_rebalance_queues, [{queues, all}]}, {"/auth", rabbit_mgmt_wm_auth, []}, + {"/auth/introspect", rabbit_mgmt_wm_oauth_introspect, []}, {"/auth/attempts/:node", rabbit_mgmt_wm_auth_attempts, [all]}, {"/auth/attempts/:node/source", rabbit_mgmt_wm_auth_attempts, [by_source]}, {"/login", rabbit_mgmt_wm_login, []}, diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_oauth_bootstrap.erl b/deps/rabbitmq_management/src/rabbit_mgmt_oauth_bootstrap.erl index e74d6530433b..85055f718a1f 100644 --- a/deps/rabbitmq_management/src/rabbit_mgmt_oauth_bootstrap.erl +++ b/deps/rabbitmq_management/src/rabbit_mgmt_oauth_bootstrap.erl @@ -9,6 +9,7 @@ -export([init/2]). -include("rabbit_mgmt.hrl"). +-include_lib("kernel/include/logger.hrl"). %%-------------------------------------------------------------------- @@ -19,32 +20,38 @@ init(Req0, State) -> bootstrap_oauth(Req0, State) -> AuthSettings = rabbit_mgmt_wm_auth:authSettings(), Dependencies = oauth_dependencies(), - {Req1, SetTokenAuth} = set_token_auth(AuthSettings, Req0), - JSContent = import_dependencies(Dependencies) ++ - set_oauth_settings(AuthSettings) ++ - SetTokenAuth ++ - export_dependencies(Dependencies), - - {ok, cowboy_req:reply(200, #{<<"content-type">> => <<"text/javascript; charset=utf-8">>}, - JSContent, Req1), State}. + case set_token_auth(AuthSettings, Req0) of + {error, Reason} -> + rabbit_mgmt_util:not_authorised(Reason, Req0, State); + {Req1, SetTokenAuth} -> + JSContent = import_dependencies(Dependencies) ++ + set_oauth_settings(AuthSettings) ++ + SetTokenAuth ++ + export_dependencies(Dependencies), + + {ok, cowboy_req:reply(200, #{<<"content-type">> => <<"text/javascript; charset=utf-8">>}, + JSContent, Req1), State} + end. set_oauth_settings(AuthSettings) -> JsonAuthSettings = rabbit_json:encode(rabbit_mgmt_format:format_nulls(AuthSettings)), ["set_oauth_settings(", JsonAuthSettings, ");"]. set_token_auth(AuthSettings, Req0) -> - case proplists:get_value(oauth_enabled, AuthSettings, false) of + ReqTokenTuple = case proplists:get_value(oauth_enabled, AuthSettings, false) of true -> case cowboy_req:parse_header(<<"authorization">>, Req0) of {bearer, Token} -> + ?LOG_DEBUG("set_token_auth bearer token ~p", [Token]), { Req0, - ["set_token_auth('", Token, "');"] + Token }; _ -> Cookies = cowboy_req:parse_cookies(Req0), case lists:keyfind(?OAUTH2_ACCESS_TOKEN_COOKIE_NAME, 1, Cookies) of - {_, Token} -> + {_, Token} -> + ?LOG_DEBUG("set_token_auth cookie token ~p", [Token]), { cowboy_req:set_resp_cookie( ?OAUTH2_ACCESS_TOKEN_COOKIE_NAME, <<"">>, Req0, #{ @@ -53,18 +60,51 @@ set_token_auth(AuthSettings, Req0) -> path => ?OAUTH2_ACCESS_TOKEN_COOKIE_PATH, same_site => strict }), - ["set_token_auth('", Token, "');"] + Token }; false -> { Req0, - [] + undefined } end end; false -> { Req0, - [] + undefined } + end, + case ReqTokenTuple of + {Req, undefined} -> {Req, []}; + {Req, Tk} -> + case oauth2_client:is_jwt_token(Tk) of + true -> + { + Req, + ["set_token_auth('", Tk, "');"] + }; + false -> + case map_opaque_to_jwt_token(Tk) of + {ok, Tk1} -> + ?LOG_DEBUG("Successfully introspected token : ~p", [Tk1]), + { + Req, + ["set_token_auth('", Tk1, "');"] + }; + {error, _} = Err1 -> + Err1 + end + end + end. + + +map_opaque_to_jwt_token(OpaqueToken) -> + case oauth2_client:introspect_token(OpaqueToken) of + {error, introspected_token_not_valid} = Error -> Error; + {ok, JwtPayload} -> + case oauth2_client:sign_token(JwtPayload) of + {ok, JWT} -> {ok, JWT}; + {error, _} = Err1 -> Err1 + end end. import_dependencies(Dependencies) -> diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_util.erl b/deps/rabbitmq_management/src/rabbit_mgmt_util.erl index 3cead5b415ae..775c8b21d4fa 100644 --- a/deps/rabbitmq_management/src/rabbit_mgmt_util.erl +++ b/deps/rabbitmq_management/src/rabbit_mgmt_util.erl @@ -17,6 +17,7 @@ is_authorized_vhost_visible/2, is_authorized_vhost_visible_for_monitoring/2, is_authorized_global_parameters/2]). +-export([not_authorised/3]). -export([user/1]). -export([bad_request/3, service_unavailable/3, bad_request_exception/4, internal_server_error/3, internal_server_error/4, precondition_failed/3, diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_oauth_introspect.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_oauth_introspect.erl new file mode 100644 index 000000000000..3a2ec7fde64e --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_oauth_introspect.erl @@ -0,0 +1,62 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. +%% + +-module(rabbit_mgmt_wm_oauth_introspect). + +-export([init/2, + content_types_accepted/2, allowed_methods/2, accept_content/2, content_types_provided/2]). +-export([variances/2]). +-include("rabbit_mgmt.hrl"). +-include_lib("kernel/include/logger.hrl"). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +%{cowboy_rest, rabbit_mgmt_headers:set_no_cache_headers( +% rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), ?MODULE), State}. + +allowed_methods(ReqData, Context) -> + {[<<"POST">>, <<"OPTIONS">>], ReqData, Context}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +accept_content(ReqData, Context) -> + rabbit_mgmt_util:post_respond(do_it(ReqData, Context)). + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +do_it(ReqData, Context) -> + ?LOG_DEBUG("Requested introspect token via management api"), + case cowboy_req:parse_header(<<"authorization">>, ReqData) of + {bearer, Token} -> + case oauth2_client:introspect_token(Token) of + {error, introspected_token_not_valid} -> + ?LOG_ERROR("Failed to introspect token due to ~p", [introspected_token_not_valid]), + rabbit_mgmt_util:not_authorised("Introspected token is not active", ReqData, Context); + {error, Reason} -> + ?LOG_ERROR("Failed to introspect token due to ~p", [Reason]), + rabbit_mgmt_util:not_authorised(Reason, ReqData, Context); + {ok, JwtPayload} -> + case oauth2_client:sign_token(JwtPayload) of + {ok, JWT} -> + rabbit_mgmt_util:reply([{token, JWT}], ReqData, Context); + {error, Reason} -> + rabbit_mgmt_util:not_authorised(Reason, ReqData, Context) + end + end; + _ -> + rabbit_mgmt_util:bad_request(<<"Opaque token not found in authorization header">>, ReqData, Context) + end. diff --git a/deps/rabbitmq_management/test/introspect_http_handler.erl b/deps/rabbitmq_management/test/introspect_http_handler.erl new file mode 100644 index 000000000000..21a7cb18c76a --- /dev/null +++ b/deps/rabbitmq_management/test/introspect_http_handler.erl @@ -0,0 +1,29 @@ +-module(introspect_http_handler). +-behavior(cowboy_handler). + +-export([init/2, terminate/3]). + +init(Req, State) -> + ct:log("introspect_http_handler init : ~p", [Req]), + case cowboy_req:read_urlencoded_body(Req) of + {ok, KeyValues, _Req} -> + ct:log("introspect_http_handler responding with active token: ~p", [KeyValues]), + case proplists:get_value(<<"token">>, KeyValues) of + <<"401">> -> + {ok, cowboy_req:reply(401, #{}, [], Req), State}; + <<"active">> -> + Body = rabbit_json:encode([{"active", true}, {"scope", "rabbitmq.tag:administrator"}]), + {ok, cowboy_req:reply(200, #{<<"content-type">> => <<"application/json">>}, + Body, Req), State}; + <<"inactive">> -> + Body = rabbit_json:encode([{"active", false}, {"scope", "rabbitmq.tag:administrator"}]), + {ok, cowboy_req:reply(200, #{<<"content-type">> => <<"application/json">>}, + Body, Req), State} + end; + Other -> + ct:log("introspect_http_handler responding with 401 : ~p", [Other]), + {ok, cowboy_req:reply(401, #{}, [], Req), State} + end. + +terminate(_Reason, _Req, _State) -> + ok. diff --git a/deps/rabbitmq_management/test/rabbit_mgmt_wm_auth_SUITE.erl b/deps/rabbitmq_management/test/rabbit_mgmt_wm_auth_SUITE.erl index eff751803315..7fdfc32e6c8a 100644 --- a/deps/rabbitmq_management/test/rabbit_mgmt_wm_auth_SUITE.erl +++ b/deps/rabbitmq_management/test/rabbit_mgmt_wm_auth_SUITE.erl @@ -11,8 +11,25 @@ -include_lib("eunit/include/eunit.hrl"). -import(application, [set_env/3, unset_env/2]). -import(rabbit_mgmt_wm_auth, [authSettings/0]). +-import(rabbit_mgmt_test_util, [req/5]). -compile(export_all). +-import(rabbit_mgmt_test_util, [assert_list/2, assert_item/2, test_item/2, + assert_keys/2, assert_no_keys/2, + decode_body/1, + http_get/2, http_get/3, http_get/5, + http_get_no_auth/3, + http_get_no_decode/5, + http_put/4, http_put/6, + http_post/4, http_post/6, + http_post_json/4, + http_upload_raw/8, + http_delete/3, http_delete/4, http_delete/5, + http_put_raw/4, http_post_accept_json/4, + req/4, auth_header/2, + assert_permanent_redirect/3, + uri_base_from/2, format_for_upload/1, + amqp_port/1, req/6]). all() -> [ {group, without_any_settings}, @@ -27,12 +44,27 @@ all() -> {group, verify_oauth_initiated_logon_type_for_idp_initiated}, {group, verify_oauth_disable_basic_auth}, {group, verify_oauth_scopes}, - {group, verify_extra_endpoint_params} + {group, verify_extra_endpoint_params}, + {group, run_with_broker} ]. groups() -> [ - + {run_with_broker, [], [ + {verify_introspection_endpoint, [], [ + introspect_opaque_token_returns_active_jwt_token, + introspect_opaque_token_returns_inactive_jwt_token, + introspect_opaque_token_returns_401_from_auth_server, + {verify_oauth_bootstrap_js, [], [ + oauth_bootstrap_with_jwt_token_in_header, + oauth_bootstrap_with_jwt_token_in_cookie, + oauth_bootstrap_with_opaque_token_in_cookie, + oauth_bootstrap_cannot_introspect_opaque_token_in_header, + oauth_bootstrap_cannot_introspect_opaque_token_in_cookie, + oauth_bootstrap_without_any_token + ]} + ]} + ]}, {verify_multi_resource_and_provider, [], [ {with_oauth_enabled, [], [ {with_oauth_providers_idp1_idp2, [], [ @@ -510,6 +542,31 @@ init_per_group(with_mgt_resource_server_a_with_token_endpoint_params_1, Config) ?config(a, Config), oauth_token_endpoint_params, ?config(token_params_1, Config)), Config; +init_per_group(run_with_broker, Config) -> + Config1 = finish_init(run_with_broker, Config), + start_broker(Config1); + +init_per_group(verify_introspection_endpoint, Config) -> + {ok, _} = application:ensure_all_started(ssl), + {ok, _} = application:ensure_all_started(cowboy), + + PortBase = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_ports_base), + Port = PortBase + 100, + AuthorizationServerURL = uri_string:normalize(#{ + scheme => "https",port => Port,path => "/introspect",host => "localhost"}), + + CertsDir = ?config(rmq_certsdir, Config), + Endpoints = [ {"/introspect", introspect_http_handler, []}], + Dispatch = cowboy_router:compile([{'_', Endpoints}]), + {ok, _} = cowboy:start_tls(introspection_http_listener, + [{port, Port}, + {certfile, filename:join([CertsDir, "server", "cert.pem"])}, + {keyfile, filename:join([CertsDir, "server", "key.pem"])}], + #{env => #{dispatch => Dispatch}}), + + [ {authorization_server_url, AuthorizationServerURL}, + {authorization_server_ca_cert, filename:join([CertsDir, "testca", "cacert.pem"])} | Config]; + init_per_group(_, Config) -> Config. @@ -632,10 +689,130 @@ end_per_group(with_mgt_resource_server_a_with_token_endpoint_params_1, Config) - ?config(a, Config), oauth_token_endpoint_params), Config; +end_per_group(run_with_broker, Config) -> + Teardown0 = rabbit_ct_client_helpers:teardown_steps(), + Teardown1 = rabbit_ct_broker_helpers:teardown_steps(), + Steps = Teardown0 ++ Teardown1, + rabbit_ct_helpers:run_teardown_steps(Config, Steps), + Config; + +end_per_group(verify_introspection_endpoint, Config) -> + ok = cowboy:stop_listener(introspection_http_listener), + inets:stop(), + Config; end_per_group(_, Config) -> Config. +init_per_testcase(Testcase, Config) when Testcase =:= introspect_opaque_token_returns_active_jwt_token orelse + Testcase =:= introspect_opaque_token_returns_inactive_jwt_token orelse + Testcase =:= introspect_opaque_token_returns_401_from_auth_server -> + + setup_introspection_configuration(Config), + rabbit_ct_helpers:testcase_started(Config, Testcase); + +init_per_testcase(Testcase, Config) when Testcase =:= oauth_bootstrap_with_jwt_token_in_header orelse + Testcase =:= oauth_bootstrap_with_jwt_token_in_cookie orelse + Testcase =:= oauth_bootstrap_with_opaque_token_in_cookie orelse + Testcase =:= oauth_bootstrap_cannot_introspect_opaque_token_in_header orelse + Testcase =:= oauth_bootstrap_cannot_introspect_opaque_token_in_cookie orelse + Testcase =:= oauth_bootstrap_without_any_token -> + rabbit_ct_helpers:testcase_started( + setup_introspection_configuration(setup_oauth2_management_configuration(Config)), Testcase); + +init_per_testcase(Testcase, Config) -> + Config. + +setup_introspection_configuration(Config) -> + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, introspection_endpoint, + ?config(authorization_server_url, Config)]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, introspection_client_id, "some-id"]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, introspection_client_secret, "some-secret"]), + CaCertFile = ?config(authorization_server_ca_cert, Config), + + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, key_config, [{cacertfile, CaCertFile}]]), + + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, opaque_token_signing_key, + [{id, <<"rabbit_key">>}, {type, hs256}, {key, <<"some-key">>}]]), + Config. + +teardown_introspection_configuration(Config) -> + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, introspection_endpoint]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, introspection_client_id]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, introspection_client_secret]), + Config. + +setup_oauth2_management_configuration(Config) -> + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management, oauth_enabled, true]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, resource_server_id, "rabbitmq"]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management, oauth_client_id, "rabbit_user"]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management, oauth_client_secret, "rabbit_secret"]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management, oauth_provider_url, "http://localhost:8080/uaa"]), + Config. + +teardown_oauth2_management_configuration(Config) -> + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_management, oauth_enabled]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_auth_backend_oauth2, resource_server_id]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_management, oauth_client_id]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_management, oauth_client_secret]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_management, oauth_provider_url]), + Config. + +end_per_testcase(Testcase, Config) when Testcase =:= introspect_opaque_token_returns_active_jwt_token orelse + Testcase =:= introspect_opaque_token_returns_inactive_jwt_token orelse + Testcase =:= introspect_opaque_token_returns_401_from_auth_server -> + teardown_introspection_configuration(Config); + +end_per_testcase(Testcase, Config) when Testcase =:= oauth_bootstrap_with_jwt_token_in_header orelse + Testcase =:= oauth_bootstrap_with_jwt_token_in_cookie orelse + Testcase =:= oauth_bootstrap_with_opaque_token_in_cookie orelse + Testcase =:= oauth_bootstrap_cannot_introspect_opaque_token_in_header orelse + Testcase =:= oauth_bootstrap_cannot_introspect_opaque_token_in_cookie orelse + Testcase =:= oauth_bootstrap_without_any_token -> + teardown_introspection_configuration(teardown_oauth2_management_configuration(Config)); + +end_per_testcase(Testcase, Config) -> + Config. + +start_broker(Config) -> + Setup0 = rabbit_ct_broker_helpers:setup_steps(), + Setup1 = rabbit_ct_client_helpers:setup_steps(), + Steps = Setup0 ++ Setup1, + case rabbit_ct_helpers:run_setup_steps(Config, Steps) of + {skip, _} = Skip -> + Skip; + Config1 -> + Ret = rabbit_ct_broker_helpers:enable_feature_flag( + Config1, 'rabbitmq_4.0.0'), + case Ret of + ok -> Config1; + _ -> Ret + end + end. +finish_init(Group, Config) -> + rabbit_ct_helpers:log_environment(), + inets:start(), + NodeConf = [{rmq_nodename_suffix, Group}], + rabbit_ct_helpers:set_config(Config, NodeConf). + %% ------------------------------------------------------------------- %% Test cases. @@ -838,6 +1015,65 @@ should_return_mgt_oauth_resource_a_with_token_endpoint_params_1(Config) -> assertEqual_on_attribute_for_oauth_resource_server(authSettings(), Config, a, oauth_token_endpoint_params, token_params_1). +introspect_opaque_token_returns_active_jwt_token(Config) -> + {ok, {{_HTTP, 200, _}, _Headers, ResBody}} = req(Config, 0, post, "/auth/introspect", [ + {"authorization", "bearer active"}], []). + +introspect_opaque_token_returns_inactive_jwt_token(Config) -> + {ok, {{_HTTP, 401, _}, _Headers, ResBody}} = req(Config, 0, post, "/auth/introspect", [ + {"authorization", "bearer inactive"}], []), + JSON = rabbit_json:decode(rabbit_data_coercion:to_binary(ResBody)), + ?assertEqual(<<"not_authorised">>, maps:get(<<"error">>, JSON)), + ?assertEqual(<<"Introspected token is not active">>, maps:get(<<"reason">>, JSON)). + +introspect_opaque_token_returns_401_from_auth_server(Config) -> + {ok, {{_HTTP, 401, _}, _Headers, _ResBody}} = req(Config, 0, post, "/auth/introspect", [ + {"authorization", "bearer 401"}], []). + +oauth_bootstrap_with_jwt_token_in_header(Config) -> + URI = rabbit_mgmt_test_util:uri_base_from(Config, 0, "") ++ "js/oidc-oauth/bootstrap.js", + Result = httpc:request(get, {URI, [{"Authorization", "bearer abc.dee.fff"}]}, [], []), + {ok, {{_HTTP, 200, _}, _Headers, ResBody}} = Result, + ct:log("resbody: ~p", [ResBody]), + case string:find(ResBody,"set_token_auth('abc.dee.fff')") of + nomatch -> ct:fail("expected setting token"); + _ -> ok + end. + +oauth_bootstrap_with_jwt_token_in_cookie(Config) -> + URI = rabbit_mgmt_test_util:uri_base_from(Config, 0, "") ++ "js/oidc-oauth/bootstrap.js", + Result = httpc:request(get, {URI, [{"cookie", "access_token=abc.dee.fff"}]}, [], []), + {ok, {{_HTTP, 200, _}, _Headers, ResBody}} = Result, + ct:log("resbody: ~p", [ResBody]), + case string:find(ResBody,"set_token_auth('abc.dee.fff')") of + nomatch -> ct:fail("expected setting token"); + _ -> ok + end. + +oauth_bootstrap_with_opaque_token_in_cookie(Config) -> + URI = rabbit_mgmt_test_util:uri_base_from(Config, 0, "") ++ "js/oidc-oauth/bootstrap.js", + Result = httpc:request(get, {URI, [{"cookie", "access_token=active"}]}, [], []), + ct:log("response idp: ~p ~p", [URI, Result]). + +oauth_bootstrap_cannot_introspect_opaque_token_in_header(Config) -> + URI = rabbit_mgmt_test_util:uri_base_from(Config, 0, "") ++ "js/oidc-oauth/bootstrap.js", + {ok, {{_HTTP, 401, _}, _Headers, _ResBody}} = + httpc:request(get, {URI, [{"Authorization", "bearer inactive"}]}, [], []). + +oauth_bootstrap_cannot_introspect_opaque_token_in_cookie(Config) -> + URI = rabbit_mgmt_test_util:uri_base_from(Config, 0, "") ++ "js/oidc-oauth/bootstrap.js", + {ok, {{_HTTP, 401, _}, _Headers, _ResBody}} = + httpc:request(get, {URI, [{"cookie", "access_token=inactive"}]}, [], []). + +oauth_bootstrap_without_any_token(Config) -> + URI = rabbit_mgmt_test_util:uri_base_from(Config, 0, "") ++ "js/oidc-oauth/bootstrap.js", + {ok, {{_HTTP, 200, _}, _Headers, ResBody}} = httpc:request(get, {URI, []}, [], []), + case string:find(ResBody,"set_token_auth(") of + nomatch -> ok; + Reminder -> ct:fail("expected no set_token_auth call") + end. + + %% ------------------------------------------------------------------- %% Utility/helper functions %% ------------------------------------------------------------------- diff --git a/selenium/authorization-server/HELP.md b/selenium/authorization-server/HELP.md new file mode 100644 index 000000000000..b929b04ec7af --- /dev/null +++ b/selenium/authorization-server/HELP.md @@ -0,0 +1,21 @@ +# Read Me First +The following was discovered as part of building this project: + +* The original package name 'com.rabbitmq.authorization-server' is invalid and this project uses 'com.rabbitmq.authorization_server' instead. + +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.5.0/maven-plugin) +* [Create an OCI image](https://docs.spring.io/spring-boot/3.5.0/maven-plugin/build-image.html) + +### Maven Parent overrides + +Due to Maven's design, elements are inherited from the parent POM to the project POM. +While most of the inheritance is fine, it also inherits unwanted elements like `` and `` from the parent. +To prevent this, the project POM contains empty overrides for these elements. +If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides. + diff --git a/selenium/bin/components/fakeportal b/selenium/bin/components/fakeportal index b0693b85a364..46889080a928 100644 --- a/selenium/bin/components/fakeportal +++ b/selenium/bin/components/fakeportal @@ -32,6 +32,8 @@ init_fakeportal() { print "> CLIENT_ID: ${CLIENT_ID}" print "> CLIENT_SECRET: ${CLIENT_SECRET}" print "> RABBITMQ_URL: ${RABBITMQ_URL}" + print "> IDP_TOKEN_ENDPOINT: ${IDP_TOKEN_ENDPOINT}" + print "> IDP: ${IDP}" } start_fakeportal() { begin "Starting fakeportal ..." @@ -48,11 +50,11 @@ start_fakeportal() { --env PORT=3000 \ --env RABBITMQ_URL="${RABBITMQ_URL_FOR_FAKEPORTAL}" \ --env PROXIED_RABBITMQ_URL="${RABBITMQ_URL}" \ - --env UAA_URL="${UAA_URL_FOR_FAKEPORTAL}" \ + --env IDP_TOKEN_ENDPOINT="${IDP_TOKEN_ENDPOINT}" \ --env CLIENT_ID="${CLIENT_ID}" \ --env CLIENT_SECRET="${CLIENT_SECRET}" \ - --env NODE_EXTRA_CA_CERTS=/etc/uaa/ca_uaa_certificate.pem \ - -v ${TEST_CONFIG_DIR}/uaa:/etc/uaa \ + --env NODE_EXTRA_CA_CERTS=/etc/${IDP}/ca_${IDP}_certificate.pem \ + -v ${TEST_CONFIG_DIR}/${IDP}:/etc/${IDP} \ -v ${FAKEPORTAL_DIR}:/code/fakeportal \ mocha-test:${mocha_test_tag} run fakeportal diff --git a/selenium/bin/components/spring b/selenium/bin/components/spring new file mode 100755 index 000000000000..05e0523dfbc9 --- /dev/null +++ b/selenium/bin/components/spring @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +SPRING_DOCKER_IMAGE=${SPRING_DOCKER_IMAGE:-pivotalrabbitmq/spring-authorization-server:0.0.10} + +ensure_spring() { + if docker ps | grep spring &> /dev/null; then + print "spring already running ..." + else + start_spring + fi +} +init_spring() { + SPRING_CONFIG_DIR=${TEST_CONFIG_PATH}/spring + SPRING_URL=${SPRING_URL:-$OAUTH_PROVIDER_URL} + + print "> SPRING_CONFIG_DIR: ${SPRING_CONFIG_DIR}" + print "> SPRING_URL: ${SPRING_URL}" + print "> SPRING_DOCKER_IMAGE: ${SPRING_DOCKER_IMAGE}" + + generate-ca-server-client-kpi spring $SPRING_CONFIG_DIR + generate-server-keystore-if-required spring $SPRING_CONFIG_DIR +} +start_spring() { + begin "Starting spring ..." + + init_spring + kill_container_if_exist spring + + MOUNT_SPRING_CONF_DIR=$CONF_DIR/spring + + mkdir -p $MOUNT_SPRING_CONF_DIR + ${BIN_DIR}/gen-spring-yml ${SPRING_CONFIG_DIR} $ENV_FILE $MOUNT_SPRING_CONF_DIR/application.yml + print "> EFFECTIVE SPRING_CONFIG_FILE: $MOUNT_SPRING_CONF_DIR/application.yml" + cp ${SPRING_CONFIG_DIR}/*.pem $MOUNT_SPRING_CONF_DIR + cp ${SPRING_CONFIG_DIR}/*.jks $MOUNT_SPRING_CONF_DIR + + docker run \ + --detach \ + --name spring \ + --net ${DOCKER_NETWORK} \ + --publish 8080:8080 \ + --publish 8443:8443 \ + -v ${MOUNT_SPRING_CONF_DIR}:/config \ + ${SPRING_DOCKER_IMAGE} + + wait_for_oidc_endpoint spring $SPRING_URL $MOUNT_SPRING_CONF_DIR/ca_spring_certificate.pem + end "spring is ready" + +} diff --git a/selenium/bin/components/uaa b/selenium/bin/components/uaa index 2a91fb468aa0..ec0cac19f63b 100644 --- a/selenium/bin/components/uaa +++ b/selenium/bin/components/uaa @@ -24,7 +24,7 @@ start_uaa() { begin "Starting UAA ..." init_uaa - kill_container_if_exist uaa + kill_container_if_exist uaa MOUNT_UAA_CONF_DIR=$CONF_DIR/uaa @@ -44,6 +44,7 @@ start_uaa() { --env JAVA_OPTS="-Djava.security.policy=unlimited -Djava.security.egd=file:/dev/./urandom" \ ${UAA_DOCKER_IMAGE} - wait_for_oidc_endpoint uaa $UAA_URL + wait_for_message uaa "Server startup in" 20 10 + wait_for_oidc_endpoint uaa $UAA_URL $MOUNT_UAA_CONF_DIR/ca_uaa_certificate.pem end "UAA is ready" } diff --git a/selenium/bin/gen-spring-yml b/selenium/bin/gen-spring-yml new file mode 100755 index 000000000000..c32d8fb1ee81 --- /dev/null +++ b/selenium/bin/gen-spring-yml @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +#set -x + +SPRING_PATH=${1:?First parameter is the directory env and config files are relative to} +ENV_FILE=${2:?Second parameter is a comma-separated list of .env file which has exported template variables} +FINAL_CONFIG_FILE=${3:?Forth parameter is the name of the final config file. It is relative to where this script is run from} + +source $ENV_FILE + +parentdir="$(dirname "$FINAL_CONFIG_FILE")" +mkdir -p $parentdir + +echo "" > $FINAL_CONFIG_FILE + +for f in $($SCRIPT/find-template-files "${PROFILES}" $SPRING_PATH "application" "yml") +do + envsubst < $f >> $FINAL_CONFIG_FILE +done diff --git a/selenium/bin/suite_template b/selenium/bin/suite_template index 3d46d26ee499..97f6b0152d55 100644 --- a/selenium/bin/suite_template +++ b/selenium/bin/suite_template @@ -154,16 +154,17 @@ build_mocha_image() { } kill_container_if_exist() { - if docker stop $1 &> /dev/null; then - docker rm $1 &> /dev/null - fi + docker kill $1 &> /dev/null + docker rm $1 &> /dev/null } wait_for_message() { - attemps_left=10 + delay=${3:-5} + attemps_left=${4:-10} + while ! docker logs $1 2>&1 | grep -q "$2"; do - sleep 5 - print "Waiting 5sec for $1 to start ($attemps_left attempts left )..." + sleep $delay + print "Waiting $delay sec for $1 to start ($attemps_left attempts left )..." ((attemps_left--)) if [[ "$attemps_left" -lt 1 ]]; then print "Timed out waiting" @@ -185,17 +186,20 @@ wait_for_oidc_endpoint() { wait_for_oidc_endpoint_local() { NAME=$1 BASE_URL=$2 - CURL_ARGS="-k --tlsv1.2 -L --fail " - DELAY_BETWEEN_ATTEMPTS=5 + CURL_ARGS="--tlsv1.2 -L --fail " + DELAY_BETWEEN_ATTEMPTS=10 if [[ $# -eq 3 ]]; then CURL_ARGS="$CURL_ARGS --cacert $3" DELAY_BETWEEN_ATTEMPTS=10 + else + CURL_ARGS="$CURL_ARGS -k " fi max_retry=15 counter=0 print "Waiting for OIDC discovery endpoint $NAME ... (BASE_URL: $BASE_URL)" until (curl $CURL_ARGS ${BASE_URL}/.well-known/openid-configuration >/dev/null 2>&1) do + echo "Failed $?" sleep $DELAY_BETWEEN_ATTEMPTS [[ counter -eq $max_retry ]] && print "Failed!" && exit 1 print "Trying again. Try #$counter" @@ -208,7 +212,7 @@ wait_for_oidc_endpoint_docker() { BASE_URL=$2 CURL_ARGS="-k --tlsv1.2 -L --fail " DOCKER_ARGS="--rm --net ${DOCKER_NETWORK} " - DELAY_BETWEEN_ATTEMPTS=5 + DELAY_BETWEEN_ATTEMPTS=10 if [[ $# -gt 2 ]]; then DOCKER_ARGS="$DOCKER_ARGS -v $3:/tmp/ca_certificate.pem" CURL_ARGS="$CURL_ARGS --cacert /tmp/ca_certificate.pem" @@ -469,7 +473,7 @@ do_generate-ca-server-client-kpi() { cd $ROOT/tls-gen/basic cp openssl.cnf openssl.cnf.bak if [ -f "$FOLDER/openssl.cnf.in" ]; then - cp $FOLDER/openssl.cnf.in >> openssl.cnf + cat $FOLDER/openssl.cnf.in >> openssl.cnf fi if [[ ! -z "${DEBUG}" ]]; then print "Used this openssl.conf" diff --git a/selenium/fakeportal/app.js b/selenium/fakeportal/app.js index 5b8d422d0375..0de515cdfc89 100644 --- a/selenium/fakeportal/app.js +++ b/selenium/fakeportal/app.js @@ -7,7 +7,7 @@ const rabbitmq_url = process.env.RABBITMQ_URL; const proxied_rabbitmq_url = process.env.PROXIED_RABBITMQ_URL; const client_id = process.env.CLIENT_ID; const client_secret = process.env.CLIENT_SECRET; -const uaa_url = process.env.UAA_URL; +const idp_token_endpoint = process.env.IDP_TOKEN_ENDPOINT; const port = process.env.PORT || 3000; app.engine('.html', require('ejs').__express); @@ -28,7 +28,10 @@ app.get('/favicon.ico', (req, res) => res.status(204)); app.listen(port); -console.log('Express started on port ' + port); +console.log('Express started on port ' + port + " using ") +console.log(" - idp_token_endpoint: " + idp_token_endpoint) +console.log(" - rabbitmq_url: " + rabbitmq_url) +console.log(" - proxied_rabbitmq_url: " + proxied_rabbitmq_url) function default_if_blank(value, defaultValue) { if (typeof value === "undefined" || value === null || value == "") { @@ -40,14 +43,13 @@ function default_if_blank(value, defaultValue) { function access_token(id, secret) { const req = new XMLHttpRequest(); - const url = uaa_url + '/oauth/token'; + const url = idp_token_endpoint const params = 'client_id=' + id + '&client_secret=' + secret + '&grant_type=client_credentials' + - '&token_format=jwt' + '&response_type=token'; - console.debug("Sending " + url + " with params "+ params); + console.debug("Sending " + url + " with params " + params); req.open('POST', url, false); req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); diff --git a/selenium/full-suite-authnz-messaging b/selenium/full-suite-authnz-messaging index 4e006e85fac1..7d5fd5282c56 100644 --- a/selenium/full-suite-authnz-messaging +++ b/selenium/full-suite-authnz-messaging @@ -7,3 +7,4 @@ authnz-messaging/auth-internal-backend.sh authnz-messaging/auth-internal-mtls-backend.sh authnz-messaging/auth-internal-http-backends.sh authnz-messaging/auth-ldap-backend.sh +authnz-messaging/auth-oauth-backend-with-opaque-tokens.sh \ No newline at end of file diff --git a/selenium/short-suite-management-ui b/selenium/short-suite-management-ui index a0d4a3a86c38..a695878d388e 100644 --- a/selenium/short-suite-management-ui +++ b/selenium/short-suite-management-ui @@ -2,7 +2,10 @@ authnz-mgt/basic-auth.sh authnz-mgt/oauth-with-keycloak.sh authnz-mgt/basic-auth-with-mgt-prefix.sh authnz-mgt/oauth-with-uaa.sh +authnz-mgt/oauth-with-spring.sh authnz-mgt/oauth-idp-initiated-with-uaa-and-prefix.sh +authnz-mgt/oauth-with-spring-with-opaque-tokens.sh +authnz-mgt/oauth-idp-initiated-with-spring-opaque-tokens.sh mgt/vhosts.sh mgt/exchanges.sh mgt/queuesAndStreams.sh diff --git a/selenium/suites/authnz-messaging/auth-cache-http-backends.sh b/selenium/suites/authnz-messaging/auth-cache-http-backends.sh index dba2c2710d93..e56e17bb9cc6 100755 --- a/selenium/suites/authnz-messaging/auth-cache-http-backends.sh +++ b/selenium/suites/authnz-messaging/auth-cache-http-backends.sh @@ -3,7 +3,7 @@ SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" TEST_CASES_PATH=/authnz-msg-protocols -PROFILES="http-user auth-http auth_backends-cache-http " +PROFILES="amqp-http-user auth-http auth_backends-cache-http " source $SCRIPT/../../bin/suite_template runWith mock-auth-backend-http diff --git a/selenium/suites/authnz-messaging/auth-cache-ldap-backends.sh b/selenium/suites/authnz-messaging/auth-cache-ldap-backends.sh index 5be7e8a62774..f81d3cf75efe 100755 --- a/selenium/suites/authnz-messaging/auth-cache-ldap-backends.sh +++ b/selenium/suites/authnz-messaging/auth-cache-ldap-backends.sh @@ -3,7 +3,7 @@ SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" TEST_CASES_PATH=/authnz-msg-protocols -PROFILES="ldap-user auth-ldap auth_backends-cache-ldap" +PROFILES="amqp-ldap-user auth-ldap auth_backends-cache-ldap" source $SCRIPT/../../bin/suite_template runWith mock-auth-backend-ldap diff --git a/selenium/suites/authnz-messaging/auth-http-backend-with-mtls.sh b/selenium/suites/authnz-messaging/auth-http-backend-with-mtls.sh index 47245df83a69..e49275f3c54f 100755 --- a/selenium/suites/authnz-messaging/auth-http-backend-with-mtls.sh +++ b/selenium/suites/authnz-messaging/auth-http-backend-with-mtls.sh @@ -3,7 +3,7 @@ SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" TEST_CASES_PATH=/authnz-msg-protocols -PROFILES="internal-user auth-http auth_backends-http auth-mtls" +PROFILES="amqp-internal-user auth-http auth_backends-http auth-mtls" # internal-user profile is used because the client certificates to # access rabbitmq are issued with the alt_name = internal-user diff --git a/selenium/suites/authnz-messaging/auth-http-internal-backends-with-internal.sh b/selenium/suites/authnz-messaging/auth-http-internal-backends-with-internal.sh index c7fb154b60bc..5fd016b2dc91 100755 --- a/selenium/suites/authnz-messaging/auth-http-internal-backends-with-internal.sh +++ b/selenium/suites/authnz-messaging/auth-http-internal-backends-with-internal.sh @@ -3,7 +3,7 @@ SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" TEST_CASES_PATH=/authnz-msg-protocols -PROFILES="internal-user auth-http auth_backends-http-internal " +PROFILES="amqp-internal-user auth-http auth_backends-http-internal " source $SCRIPT/../../bin/suite_template runWith mock-auth-backend-http diff --git a/selenium/suites/authnz-messaging/auth-http-internal-backends.sh b/selenium/suites/authnz-messaging/auth-http-internal-backends.sh index 105926e117dc..2b4b5f290a83 100755 --- a/selenium/suites/authnz-messaging/auth-http-internal-backends.sh +++ b/selenium/suites/authnz-messaging/auth-http-internal-backends.sh @@ -3,7 +3,7 @@ SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" TEST_CASES_PATH=/authnz-msg-protocols -PROFILES="http-user auth-http auth_backends-http-internal " +PROFILES="amqp-http-user auth-http auth_backends-http-internal " source $SCRIPT/../../bin/suite_template runWith mock-auth-backend-http diff --git a/selenium/suites/authnz-messaging/auth-internal-backend.sh b/selenium/suites/authnz-messaging/auth-internal-backend.sh index b513001e1f6c..804b579f55ef 100755 --- a/selenium/suites/authnz-messaging/auth-internal-backend.sh +++ b/selenium/suites/authnz-messaging/auth-internal-backend.sh @@ -3,7 +3,7 @@ SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" TEST_CASES_PATH=/authnz-msg-protocols -PROFILES="internal-user auth_backends-internal" +PROFILES="amqp-internal-user auth_backends-internal" source $SCRIPT/../../bin/suite_template run diff --git a/selenium/suites/authnz-messaging/auth-internal-http-backends.sh b/selenium/suites/authnz-messaging/auth-internal-http-backends.sh index ae44cf8e8dc6..221a32ef06ab 100755 --- a/selenium/suites/authnz-messaging/auth-internal-http-backends.sh +++ b/selenium/suites/authnz-messaging/auth-internal-http-backends.sh @@ -3,7 +3,7 @@ SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" TEST_CASES_PATH=/authnz-msg-protocols -PROFILES="internal-user auth_http auth_backends-internal-http " +PROFILES="amqp-internal-user auth_http auth_backends-internal-http " source $SCRIPT/../../bin/suite_template runWith mock-auth-backend-http diff --git a/selenium/suites/authnz-messaging/auth-internal-mtls-backend.sh b/selenium/suites/authnz-messaging/auth-internal-mtls-backend.sh index df92f9d9cd43..687f7f269b18 100755 --- a/selenium/suites/authnz-messaging/auth-internal-mtls-backend.sh +++ b/selenium/suites/authnz-messaging/auth-internal-mtls-backend.sh @@ -3,7 +3,7 @@ SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" TEST_CASES_PATH=/authnz-msg-protocols -PROFILES="internal-user auth_backends-internal tls auth-mtls" +PROFILES="amqp-internal-user auth_backends-internal tls auth-mtls" source $SCRIPT/../../bin/suite_template run diff --git a/selenium/suites/authnz-messaging/auth-ldap-backend.sh b/selenium/suites/authnz-messaging/auth-ldap-backend.sh index 8ecc52cc1dab..387633ccbfc2 100755 --- a/selenium/suites/authnz-messaging/auth-ldap-backend.sh +++ b/selenium/suites/authnz-messaging/auth-ldap-backend.sh @@ -3,7 +3,7 @@ SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" TEST_CASES_PATH=/authnz-msg-protocols -PROFILES="ldap-user auth-ldap auth_backends-ldap " +PROFILES="amqp-ldap-user auth-ldap auth_backends-ldap " source $SCRIPT/../../bin/suite_template runWith mock-auth-backend-ldap diff --git a/selenium/suites/authnz-messaging/auth-oauth-backend-with-opaque-tokens.sh b/selenium/suites/authnz-messaging/auth-oauth-backend-with-opaque-tokens.sh new file mode 100755 index 000000000000..cee200646eed --- /dev/null +++ b/selenium/suites/authnz-messaging/auth-oauth-backend-with-opaque-tokens.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +TEST_CASES_PATH=/authnz-msg-protocols +PROFILES="spring oauth-producer auth-oauth-spring auth_backends-opaque-oauth " + +source $SCRIPT/../../bin/suite_template +runWith spring \ No newline at end of file diff --git a/selenium/suites/authnz-messaging/spring/application.yml b/selenium/suites/authnz-messaging/spring/application.yml new file mode 100644 index 000000000000..31e2e96b4e0e --- /dev/null +++ b/selenium/suites/authnz-messaging/spring/application.yml @@ -0,0 +1,37 @@ +server: + port: 8443 + ssl: + bundle: spring-authorizationserver + +spring: + ssl: + bundle: + jks: + spring-authorizationserver: + key: + alias: server-spring-tls + password: foobar + keystore: + location: /config/server_spring.jks + password: foobar + type: PKCS12 + security: + oauth2: + authorizationserver: + client: + producer: + registration: + provider: spring + client-id: producer + client-secret: producer + authorization-grant-types: + - client_credentials + client-authentication-methods: + - client_secret_basic + scopes: + - rabbitmq.configure:*/* + - rabbitmq.read:*/* + - rabbitmq.write:*/* + client-name: producer + token: + access-token-format: reference diff --git a/selenium/suites/authnz-messaging/spring/openssl.cnf.in b/selenium/suites/authnz-messaging/spring/openssl.cnf.in new file mode 100644 index 000000000000..5ac3282046c5 --- /dev/null +++ b/selenium/suites/authnz-messaging/spring/openssl.cnf.in @@ -0,0 +1,3 @@ +[ client_alt_names ] +email.1 = rabbit_client@localhost +URI.1 = rabbit_client_id_uri diff --git a/selenium/suites/authnz-mgt/oauth-idp-initiated-with-spring-opaque-tokens.sh b/selenium/suites/authnz-mgt/oauth-idp-initiated-with-spring-opaque-tokens.sh new file mode 100755 index 000000000000..26b7ce542247 --- /dev/null +++ b/selenium/suites/authnz-mgt/oauth-idp-initiated-with-spring-opaque-tokens.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +TEST_CASES_PATH=/oauth/with-idp-initiated +TEST_CONFIG_PATH=/oauth +PROFILES="spring idp-initiated spring-oauth-provider fakeportal-mgt-oauth-provider opaque-token" + +source $SCRIPT/../../bin/suite_template $@ +runWith spring fakeportal diff --git a/selenium/suites/authnz-mgt/oauth-with-spring-with-opaque-tokens.sh b/selenium/suites/authnz-mgt/oauth-with-spring-with-opaque-tokens.sh new file mode 100755 index 000000000000..c550c5da1e79 --- /dev/null +++ b/selenium/suites/authnz-mgt/oauth-with-spring-with-opaque-tokens.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +TEST_CASES_PATH=/oauth/with-sp-initiated +TEST_CONFIG_PATH=/oauth +PROFILES="spring spring-oauth-provider tls opaque-token" + +source $SCRIPT/../../bin/suite_template $@ +runWith spring diff --git a/selenium/suites/authnz-mgt/oauth-with-spring.sh b/selenium/suites/authnz-mgt/oauth-with-spring.sh new file mode 100755 index 000000000000..583875f677d3 --- /dev/null +++ b/selenium/suites/authnz-mgt/oauth-with-spring.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +TEST_CASES_PATH=/oauth/with-sp-initiated +TEST_CONFIG_PATH=/oauth +PROFILES="spring spring-oauth-provider tls" + +source $SCRIPT/../../bin/suite_template $@ +runWith spring diff --git a/selenium/test/amqp.js b/selenium/test/amqp.js index cb94bfdfc983..c740caecfbbb 100644 --- a/selenium/test/amqp.js +++ b/selenium/test/amqp.js @@ -41,6 +41,7 @@ function getConnectionOptions() { } module.exports = { getAmqpConnectionOptions: () => { return connectionOptions }, + setAmqpConnectionOptions: (options) => { connectionOptions = options }, getAmqpUrl: () => { return connectionOptions.scheme + '://' + connectionOptions.username + ":" + connectionOptions.password + "@" + diff --git a/selenium/test/authnz-msg-protocols/amqp10.js b/selenium/test/authnz-msg-protocols/amqp10.js index 714389bcb73f..2278b6d1c43a 100644 --- a/selenium/test/authnz-msg-protocols/amqp10.js +++ b/selenium/test/authnz-msg-protocols/amqp10.js @@ -1,7 +1,9 @@ const assert = require('assert') const { log, tokenFor, openIdConfiguration } = require('../utils') const { reset, expectUser, expectVhost, expectResource, allow, verifyAll } = require('../mock_http_backend') -const { open: openAmqp, once: onceAmqp, on: onAmqp, close: closeAmqp } = require('../amqp') +const { getAmqpConnectionOptions: getAmqpOptions, + setAmqpConnectionOptions: setAmqpOptions, + open: openAmqp, once: onceAmqp, on: onAmqp, close: closeAmqp } = require('../amqp') var receivedAmqpMessageCount = 0 var untilConnectionEstablished = new Promise((resolve, reject) => { @@ -31,6 +33,7 @@ describe('Having AMQP 1.0 protocol enabled and the following auth_backends: ' + let password = process.env.RABBITMQ_AMQP_PASSWORD let usemtls = process.env.AMQP_USE_MTLS let amqp; + let amqpSettings = getAmqpOptions() before(function () { if (backends.includes("http") && (username.includes("http") || usemtls)) { @@ -48,11 +51,17 @@ describe('Having AMQP 1.0 protocol enabled and the following auth_backends: ' + let oauthProviderUrl = process.env.OAUTH_PROVIDER_URL let oauthClientId = process.env.OAUTH_CLIENT_ID let oauthClientSecret = process.env.OAUTH_CLIENT_SECRET + let scopes = process.env.OAUTH_SCOPES log("oauthProviderUrl : " + oauthProviderUrl) + log("oauthClientId : " + oauthClientId) + log("oauthClientSecret : " + oauthClientSecret) + log("oauthScope : " + scopes) let openIdConfig = openIdConfiguration(oauthProviderUrl) log("Obtained token_endpoint : " + openIdConfig.token_endpoint) - password = tokenFor(oauthClientId, oauthClientSecret, openIdConfig.token_endpoint) + password = tokenFor(oauthClientId, oauthClientSecret, openIdConfig.token_endpoint, scopes) log("Obtained access token : " + password) + amqpSettings.password = password + setAmqpOptions(amqpSettings) } }) diff --git a/selenium/test/authnz-msg-protocols/env.http-user b/selenium/test/authnz-msg-protocols/env.amqp-http-user similarity index 100% rename from selenium/test/authnz-msg-protocols/env.http-user rename to selenium/test/authnz-msg-protocols/env.amqp-http-user diff --git a/selenium/test/authnz-msg-protocols/env.internal-user b/selenium/test/authnz-msg-protocols/env.amqp-internal-user similarity index 100% rename from selenium/test/authnz-msg-protocols/env.internal-user rename to selenium/test/authnz-msg-protocols/env.amqp-internal-user diff --git a/selenium/test/authnz-msg-protocols/env.ldap-user b/selenium/test/authnz-msg-protocols/env.amqp-ldap-user similarity index 100% rename from selenium/test/authnz-msg-protocols/env.ldap-user rename to selenium/test/authnz-msg-protocols/env.amqp-ldap-user diff --git a/selenium/test/authnz-msg-protocols/env.auth-oauth-spring.docker b/selenium/test/authnz-msg-protocols/env.auth-oauth-spring.docker new file mode 100644 index 000000000000..832e0eabf2c3 --- /dev/null +++ b/selenium/test/authnz-msg-protocols/env.auth-oauth-spring.docker @@ -0,0 +1,4 @@ +export SPRING_URL=https://spring:8443 +export OAUTH_PROVIDER_URL=https://spring:8443 +export SPRING_CA_CERT=/config/authnz-msg-protocols/spring/ca_spring_certificate.pem +export OAUTH_NODE_EXTRA_CA_CERTS=authnz-msg-protocols/spring/ca_spring_certificate.pem diff --git a/selenium/test/authnz-msg-protocols/env.auth-oauth-spring.local b/selenium/test/authnz-msg-protocols/env.auth-oauth-spring.local new file mode 100644 index 000000000000..4653299091a4 --- /dev/null +++ b/selenium/test/authnz-msg-protocols/env.auth-oauth-spring.local @@ -0,0 +1,4 @@ +export SPRING_URL=https://localhost:8443 +export OAUTH_PROVIDER_URL=https://localhost:8443 +export SPRING_CA_CERT=${TEST_CONFIG_PATH}/spring/ca_spring_certificate.pem +export OAUTH_NODE_EXTRA_CA_CERTS=authnz-msg-protocols/spring/ca_spring_certificate.pem diff --git a/selenium/test/authnz-msg-protocols/env.auth_backends-opaque-oauth b/selenium/test/authnz-msg-protocols/env.auth_backends-opaque-oauth new file mode 100644 index 000000000000..0784844dbf1d --- /dev/null +++ b/selenium/test/authnz-msg-protocols/env.auth_backends-opaque-oauth @@ -0,0 +1 @@ +OAUTH_TOKEN_FORMAT=opaque diff --git a/selenium/test/authnz-msg-protocols/env.oauth-producer b/selenium/test/authnz-msg-protocols/env.oauth-producer new file mode 100644 index 000000000000..cf44f442b8d5 --- /dev/null +++ b/selenium/test/authnz-msg-protocols/env.oauth-producer @@ -0,0 +1,4 @@ +export RABBITMQ_AMQP_USERNAME=oauth +export OAUTH_CLIENT_ID=producer +export OAUTH_CLIENT_SECRET=producer +export OAUTH_SCOPES="rabbitmq.configure:*/* rabbitmq.read:*/* rabbitmq.write:*/*" diff --git a/selenium/test/authnz-msg-protocols/mqtt.js b/selenium/test/authnz-msg-protocols/mqtt.js index c6466a919d5a..ce5cb47abb3c 100644 --- a/selenium/test/authnz-msg-protocols/mqtt.js +++ b/selenium/test/authnz-msg-protocols/mqtt.js @@ -44,9 +44,10 @@ describe('Having MQTT protocol enbled and the following auth_backends: ' + backe let oauthProviderUrl = process.env.OAUTH_PROVIDER_URL let oauthClientId = process.env.OAUTH_CLIENT_ID let oauthClientSecret = process.env.OAUTH_CLIENT_SECRET + let scope = process.env.OAUTH_SCOPES let openIdConfig = openIdConfiguration(oauthProviderUrl) log("Obtained token_endpoint : " + openIdConfig.token_endpoint) - password = tokenFor(oauthClientId, oauthClientSecret, openIdConfig.token_endpoint) + password = tokenFor(oauthClientId, oauthClientSecret, openIdConfig.token_endpoint, scope) log("Obtained access token : " + password) } mqttOptions = { diff --git a/selenium/test/authnz-msg-protocols/rabbitmq.auth_backends-opaque-oauth.conf b/selenium/test/authnz-msg-protocols/rabbitmq.auth_backends-opaque-oauth.conf new file mode 100644 index 000000000000..1787ea88d0a1 --- /dev/null +++ b/selenium/test/authnz-msg-protocols/rabbitmq.auth_backends-opaque-oauth.conf @@ -0,0 +1,24 @@ +log.console.level = debug + +auth_backends.1 = rabbit_auth_backend_oauth2 + +# Common auth_oauth2 settings for all resources +auth_oauth2.preferred_username_claims.1 = sub + +## Resource server hosted by this rabbitmq instance +auth_oauth2.resource_server_id = rabbitmq +auth_oauth2.verify_aud = false +auth_oauth2.issuer = ${SPRING_URL} +auth_oauth2.https.cacertfile = ${SPRING_CA_CERT} +auth_oauth2.https.verify = verify_peer +auth_oauth2.https.hostname_verification = wildcard + +auth_oauth2.introspection_client_auth_method = basic +auth_oauth2.introspection_client_id = introspection_client +auth_oauth2.introspection_client_secret = introspection_client + +auth_oauth2.opaque_token_signing_key.id = rabbit_opaque_key +auth_oauth2.opaque_token_signing_key.type = hs256 +auth_oauth2.opaque_token_signing_key.key = symmetrical-signing-key + +auth_oauth2.verify_aud = false diff --git a/selenium/test/authnz-msg-protocols/spring/application.yml b/selenium/test/authnz-msg-protocols/spring/application.yml new file mode 100644 index 000000000000..ff6a4116349a --- /dev/null +++ b/selenium/test/authnz-msg-protocols/spring/application.yml @@ -0,0 +1,132 @@ +server: + port: 8443 + ssl: + bundle: spring-authorizationserver + +spring: + ssl: + bundle: + jks: + spring-authorizationserver: + key: + alias: server-spring-tls + password: foobar + keystore: + location: /config/server_spring.jks + password: foobar + type: PKCS12 + security: + oauth2: + users: + - username: rabbit_admin + password: rabbit_admin + scopes: + - openid + - profile + - rabbitmq.tag:administrator + audiencies: + - rabbitmq + authorizationserver: + client: + introspection_client: + registration: + provider: spring + client-id: introspection_client + client-secret: "{noop}introspection_client" + authorization-grant-types: + - client_credentials + client-authentication-methods: + - client_secret_basic + client-name: introspection_client + producer: + registration: + provider: spring + client-id: producer + client-secret: "{noop}producer" + authorization-grant-types: + - client_credentials + client-authentication-methods: + - client_secret_basic + scopes: + - openid + - profile + - rabbitmq.tag:management + - rabbitmq.configure:*/* + - rabbitmq.read:*/* + - rabbitmq.write:*/* + client-name: producer + token: + access-token-format: reference + mgt_api_client_opaque: + registration: + provider: spring + client-id: mgt_api_client_opaque + client-secret: "{noop}mgt_api_client_opaque" + authorization-grant-types: + - client_credentials + client-authentication-methods: + - client_secret_basic + scopes: + - openid + - profile + - rabbitmq.tag:management + client-name: mgt_api_client_opaque + token: + access-token-format: reference + mgt_api_client: + registration: + provider: spring + client-id: mgt_api_client + client-secret: "{noop}mgt_api_client" + authorization-grant-types: + - client_credentials + client-authentication-methods: + - client_secret_basic + scopes: + - openid + - profile + - rabbitmq.tag:management + - rabbitmq.tag:administrator + client-name: mgt_api_client + rabbitmq_client_code_opaque: + registration: + provider: spring + client-id: rabbitmq_client_code_opaque + client-secret: "{noop}rabbitmq_client_code_opaque" + require-proof-key: true + authorization-grant-types: + - authorization_code + client-authentication-methods: + - none + redirect-uris: + - "${RABBITMQ_SCHEME}://${RABBITMQ_HOST}${RABBITMQ_PATH}/js/oidc-oauth/login-callback.html" + post-logout-redirect-uris: + - "${RABBITMQ_SCHEME}://${RABBITMQ_HOST}${RABBITMQ_PATH}/" + scopes: + - openid + - profile + - rabbitmq.tag:administrator + - rabbitmq.tag:management + client-name: rabbitmq_client_code_opaque + token: + access-token-format: reference + rabbitmq_client_code: + registration: + provider: spring + client-id: rabbitmq_client_code + require-proof-key: true + authorization-grant-types: + - authorization_code + client-authentication-methods: + - none + redirect-uris: + - "${RABBITMQ_SCHEME}://${RABBITMQ_HOST}${RABBITMQ_PATH}/js/oidc-oauth/login-callback.html" + post-logout-redirect-uris: + - "${RABBITMQ_SCHEME}://${RABBITMQ_HOST}${RABBITMQ_PATH}/" + scopes: + - openid + - profile + - rabbitmq.tag:administrator + - rabbitmq.tag:management + client-name: rabbitmq_client_code + \ No newline at end of file diff --git a/selenium/test/authnz-msg-protocols/spring/openssl.cnf.in b/selenium/test/authnz-msg-protocols/spring/openssl.cnf.in new file mode 100644 index 000000000000..f934303b6a3f --- /dev/null +++ b/selenium/test/authnz-msg-protocols/spring/openssl.cnf.in @@ -0,0 +1,3 @@ +[ client_alt_names ] +email.1 = rabbit_client@localhost +URI.1 = rabbit_client_id_uri \ No newline at end of file diff --git a/selenium/test/oauth/env.docker.fakeportal.spring b/selenium/test/oauth/env.docker.fakeportal.spring new file mode 100644 index 000000000000..fe8494e5bb05 --- /dev/null +++ b/selenium/test/oauth/env.docker.fakeportal.spring @@ -0,0 +1 @@ +export IDP_TOKEN_ENDPOINT=https://spring:8443/oauth2/token diff --git a/selenium/test/oauth/env.docker.fakeportal.uaa b/selenium/test/oauth/env.docker.fakeportal.uaa new file mode 100644 index 000000000000..123af93ecabe --- /dev/null +++ b/selenium/test/oauth/env.docker.fakeportal.uaa @@ -0,0 +1 @@ +export IDP_TOKEN_ENDPOINT=https://uaa:8443/oauth/token diff --git a/selenium/test/oauth/env.docker.spring b/selenium/test/oauth/env.docker.spring new file mode 100644 index 000000000000..78a0af0cc08a --- /dev/null +++ b/selenium/test/oauth/env.docker.spring @@ -0,0 +1,2 @@ +export SPRING_URL=https://spring:8443 +export SPRING_CA_CERT=/config/oauth/spring/ca_spring_certificate.pem diff --git a/selenium/test/oauth/env.keycloak b/selenium/test/oauth/env.keycloak index 7025c2930a28..95eaf83b91d3 100644 --- a/selenium/test/oauth/env.keycloak +++ b/selenium/test/oauth/env.keycloak @@ -1,3 +1,4 @@ export OAUTH_SERVER_CONFIG_DIR=${OAUTH_SERVER_CONFIG_BASEDIR}/oauth/keycloak export OAUTH_SIGNING_KEY_ID=Gnl2ZlbRh3rAr6Wymc988_5cY7T5GuePd5dpJlXDJUk export OAUTH_SCOPES="openid profile rabbitmq.tag:management" +export OAUTH_CLIENT_ID=rabbitmq_client_code diff --git a/selenium/test/oauth/env.local.fakeportal.spring b/selenium/test/oauth/env.local.fakeportal.spring new file mode 100644 index 000000000000..fe8494e5bb05 --- /dev/null +++ b/selenium/test/oauth/env.local.fakeportal.spring @@ -0,0 +1 @@ +export IDP_TOKEN_ENDPOINT=https://spring:8443/oauth2/token diff --git a/selenium/test/oauth/env.local.fakeportal.uaa b/selenium/test/oauth/env.local.fakeportal.uaa new file mode 100644 index 000000000000..123af93ecabe --- /dev/null +++ b/selenium/test/oauth/env.local.fakeportal.uaa @@ -0,0 +1 @@ +export IDP_TOKEN_ENDPOINT=https://uaa:8443/oauth/token diff --git a/selenium/test/oauth/env.local.spring b/selenium/test/oauth/env.local.spring new file mode 100644 index 000000000000..20f8c16cd943 --- /dev/null +++ b/selenium/test/oauth/env.local.spring @@ -0,0 +1,3 @@ +export SPRING_URL=https://localhost:8443 +export OAUTH_PROVIDER_URL=${SPRING_URL} +export SPRING_CA_CERT=selenium/test/oauth/spring/ca_spring_certificate.pem diff --git a/selenium/test/oauth/env.spring b/selenium/test/oauth/env.spring new file mode 100644 index 000000000000..e0d13ba26491 --- /dev/null +++ b/selenium/test/oauth/env.spring @@ -0,0 +1,4 @@ +export OAUTH_SERVER_CONFIG_DIR=${OAUTH_SERVER_CONFIG_BASEDIR}/oauth/spring +export OAUTH_SCOPES="openid profile" +export OAUTH_CLIENT_ID=rabbitmq_client_code +export IDP=spring diff --git a/selenium/test/oauth/env.spring-oauth-provider b/selenium/test/oauth/env.spring-oauth-provider new file mode 100644 index 000000000000..979500cb8bd1 --- /dev/null +++ b/selenium/test/oauth/env.spring-oauth-provider @@ -0,0 +1,2 @@ +export OAUTH_PROVIDER_URL=${SPRING_URL} +export OAUTH_PROVIDER_CA_CERT=${SPRING_CA_CERT} diff --git a/selenium/test/oauth/env.spring.idp-initiated.opaque-token b/selenium/test/oauth/env.spring.idp-initiated.opaque-token new file mode 100644 index 000000000000..49307dd52800 --- /dev/null +++ b/selenium/test/oauth/env.spring.idp-initiated.opaque-token @@ -0,0 +1,4 @@ +export MGT_CLIENT_ID_FOR_IDP_INITIATED=rabbit_idp_user +export MGT_CLIENT_SECRET_FOR_IDP_INITIATED=rabbit_idp_user +export MGT_UNAUTHORIZED_CLIENT_ID_FOR_IDP_INITIATED=producer_opaque +export MGT_UNAUTHORIZED_CLIENT_SECRET_FOR_IDP_INITIATED=producer_opaque diff --git a/selenium/test/oauth/env.spring.opaque-token b/selenium/test/oauth/env.spring.opaque-token new file mode 100644 index 000000000000..eef87d3234b9 --- /dev/null +++ b/selenium/test/oauth/env.spring.opaque-token @@ -0,0 +1 @@ +export OAUTH_CLIENT_ID=rabbitmq_client_code_opaque diff --git a/selenium/test/oauth/env.uaa b/selenium/test/oauth/env.uaa index 506e68ac66f7..0714e6ece341 100644 --- a/selenium/test/oauth/env.uaa +++ b/selenium/test/oauth/env.uaa @@ -1,4 +1,6 @@ export OAUTH_SIGNING_KEY_ID=legacy-token-key export OAUTH_SERVER_CONFIG_DIR=${OAUTH_SERVER_CONFIG_BASEDIR}/oauth/uaa export OAUTH_CLIENT_SECRET=rabbitmq_client_code +export OAUTH_CLIENT_ID=rabbitmq_client_code export OAUTH_SCOPES="openid profile rabbitmq.*" +export IDP=uaa diff --git a/selenium/test/oauth/rabbitmq.conf b/selenium/test/oauth/rabbitmq.conf index f101bae111c0..f9e4ca3f4193 100644 --- a/selenium/test/oauth/rabbitmq.conf +++ b/selenium/test/oauth/rabbitmq.conf @@ -1,8 +1,10 @@ auth_backends.1 = rabbit_auth_backend_oauth2 +log.console.level = debug + management.login_session_timeout = 1 management.oauth_enabled = true -management.oauth_client_id = rabbitmq_client_code +management.oauth_client_id = ${OAUTH_CLIENT_ID} management.oauth_scopes = ${OAUTH_SCOPES} management.cors.allow_origins.1 = * diff --git a/selenium/test/oauth/rabbitmq.opaque-token.conf b/selenium/test/oauth/rabbitmq.opaque-token.conf new file mode 100644 index 000000000000..b81d9af9007a --- /dev/null +++ b/selenium/test/oauth/rabbitmq.opaque-token.conf @@ -0,0 +1,9 @@ +auth_oauth2.introspection_client_auth_method = basic +auth_oauth2.introspection_client_id = introspection_client +auth_oauth2.introspection_client_secret = introspection_client + +auth_oauth2.opaque_token_signing_key.id = rabbit_opaque_key +auth_oauth2.opaque_token_signing_key.type = hs256 +auth_oauth2.opaque_token_signing_key.key = symmetrical-signing-key + +auth_oauth2.verify_aud = false diff --git a/selenium/test/oauth/rabbitmq.spring-oauth-provider.conf b/selenium/test/oauth/rabbitmq.spring-oauth-provider.conf new file mode 100644 index 000000000000..f4a8b882de38 --- /dev/null +++ b/selenium/test/oauth/rabbitmq.spring-oauth-provider.conf @@ -0,0 +1,3 @@ +auth_oauth2.issuer = ${SPRING_URL} +auth_oauth2.https.cacertfile = ${SPRING_CA_CERT} +auth_oauth2.additional_scopes_key = extra_scope diff --git a/selenium/test/oauth/spring/application.yml b/selenium/test/oauth/spring/application.yml new file mode 100644 index 000000000000..cfd53d10f9f5 --- /dev/null +++ b/selenium/test/oauth/spring/application.yml @@ -0,0 +1,179 @@ +logging.level.com.rabbitmq.authorization_server: "debug" + +server: + port: 8443 + ssl: + bundle: spring-authorizationserver + +spring: + ssl: + bundle: + jks: + spring-authorizationserver: + key: + alias: server-spring-tls + password: foobar + keystore: + location: /config/server_spring.jks + password: foobar + type: PKCS12 + security: + oauth2: + users: + - username: rabbitmq_management + password: rabbitmq_management + scopes: + - rabbitmq.read:*/* + - rabbitmq.write:*/* + - rabbitmq.configure:*/* + - rabbitmq.tag:management + audiencies: + - rabbitmq + - username: rabbit_no_management + password: rabbit_no_management + scopes: + - rabbitmq.read:*/* + audiencies: + - rabbitmq + - username: rabbit_admin + password: rabbit_admin + scopes: + - rabbitmq.tag:administrator + audiencies: + - rabbitmq + authorizationserver: + client: + introspection_client: + registration: + provider: spring + client-id: introspection_client + client-secret: "{noop}introspection_client" + authorization-grant-types: + - client_credentials + client-authentication-methods: + - client_secret_basic + client-name: introspection_client + mgt_api_client_opaque: + registration: + provider: spring + client-id: mgt_api_client_opaque + client-secret: "{noop}mgt_api_client_opaque" + authorization-grant-types: + - client_credentials + client-authentication-methods: + - client_secret_basic + scopes: + - openid + - profile + client-name: mgt_api_client_opaque + token: + access-token-format: reference + producer_opaque: + registration: + provider: spring + client-id: producer_opaque + client-secret: "{noop}producer_opaque" + authorization-grant-types: + - client_credentials + client-authentication-methods: + - client_secret_basic + - client_secret_post + scopes: + - openid + - profile + - rabbitmq.read:*/* + - rabbitmq.write:*/* + - rabbitmq.configure:*/* + client-name: producer_opaque + token: + access-token-format: reference + producer: + registration: + provider: spring + client-id: producer + client-secret: "{noop}producer" + authorization-grant-types: + - client_credentials + client-authentication-methods: + - client_secret_basic + - client_secret_post + scopes: + - openid + - profile + - rabbitmq.read:*/* + - rabbitmq.write:*/* + - rabbitmq.configure:*/* + client-name: producer + rabbit_idp_user: + registration: + provider: spring + client-id: rabbit_idp_user + client-secret: "{noop}rabbit_idp_user" + authorization-grant-types: + - client_credentials + client-authentication-methods: + - client_secret_basic + - client_secret_post + scopes: + - openid + - profile + - rabbitmq.tag:management + - rabbitmq.tag:administrator + client-name: rabbit_idp_user + audiencies: + - rabbitmq + token: + access-token-format: reference + mgt_api_client: + registration: + provider: spring + client-id: mgt_api_client + client-secret: "{noop}mgt_api_client" + authorization-grant-types: + - client_credentials + client-authentication-methods: + - client_secret_basic + scopes: + - openid + - profile + - rabbitmq.tag:management + - rabbitmq.tag:administrator + client-name: mgt_api_client + rabbitmq_client_code_opaque: + registration: + provider: spring + client-id: rabbitmq_client_code_opaque + client-secret: "{noop}rabbitmq_client_code_opaque" + require-proof-key: true + authorization-grant-types: + - authorization_code + client-authentication-methods: + - none + redirect-uris: + - "${RABBITMQ_SCHEME}://${RABBITMQ_HOST}${RABBITMQ_PATH}/js/oidc-oauth/login-callback.html" + post-logout-redirect-uris: + - "${RABBITMQ_SCHEME}://${RABBITMQ_HOST}${RABBITMQ_PATH}/" + scopes: + - openid + - profile + client-name: rabbitmq_client_code_opaque + token: + access-token-format: reference + rabbitmq_client_code: + registration: + provider: spring + client-id: rabbitmq_client_code + require-proof-key: true + authorization-grant-types: + - authorization_code + client-authentication-methods: + - none + redirect-uris: + - "${RABBITMQ_SCHEME}://${RABBITMQ_HOST}${RABBITMQ_PATH}/js/oidc-oauth/login-callback.html" + post-logout-redirect-uris: + - "${RABBITMQ_SCHEME}://${RABBITMQ_HOST}${RABBITMQ_PATH}/" + scopes: + - openid + - profile + client-name: rabbitmq_client_code + \ No newline at end of file diff --git a/selenium/test/oauth/spring/openssl.cnf.in b/selenium/test/oauth/spring/openssl.cnf.in new file mode 100644 index 000000000000..5ac3282046c5 --- /dev/null +++ b/selenium/test/oauth/spring/openssl.cnf.in @@ -0,0 +1,3 @@ +[ client_alt_names ] +email.1 = rabbit_client@localhost +URI.1 = rabbit_client_id_uri diff --git a/selenium/test/oauth/with-idp-initiated/happy-login.js b/selenium/test/oauth/with-idp-initiated/happy-login.js index ae668653d792..0f3b88383483 100644 --- a/selenium/test/oauth/with-idp-initiated/happy-login.js +++ b/selenium/test/oauth/with-idp-initiated/happy-login.js @@ -9,11 +9,15 @@ const FakePortalPage = require('../../pageobjects/FakePortalPage') describe('A user with a JWT token', function () { let overview let captureScreen - let token let fakePortal let driver + let username + let password + before(async function () { + username = process.env.MGT_CLIENT_ID_FOR_IDP_INITIATED || 'rabbit_idp_user' + password = process.env.MGT_CLIENT_SECRET_FOR_IDP_INITIATED || 'rabbit_idp_user' driver = buildDriver() overview = new OverviewPage(driver) captureScreen = captureScreensFor(driver, __filename) @@ -21,13 +25,13 @@ describe('A user with a JWT token', function () { }) it('can log in presenting the token to the /login URL via fakeportal', async function () { - await fakePortal.goToHome("rabbit_idp_user", "rabbit_idp_user") + await fakePortal.goToHome(username, password) if (!await fakePortal.isLoaded()) { throw new Error('Failed to load fakePortal') } await fakePortal.login() await overview.isLoaded() - assert.equal(await overview.getUser(), 'User rabbit_idp_user') + assert.equal(await overview.getUser(), 'User ' + username) }) after(async function () { diff --git a/selenium/test/oauth/with-idp-initiated/logout.js b/selenium/test/oauth/with-idp-initiated/logout.js index a37c40f283d8..da1d7c43957b 100644 --- a/selenium/test/oauth/with-idp-initiated/logout.js +++ b/selenium/test/oauth/with-idp-initiated/logout.js @@ -10,14 +10,19 @@ describe('When a logged in user', function () { let overview let fakePortal let captureScreen + let username + let password before(async function () { driver = buildDriver() + username = process.env.MGT_CLIENT_ID_FOR_IDP_INITIATED || 'rabbit_idp_user' + password = process.env.MGT_CLIENT_SECRET_FOR_IDP_INITIATED || 'rabbit_idp_user' + fakePortal = new FakePortalPage(driver) overview = new OverviewPage(driver) captureScreen = captureScreensFor(driver, __filename) - await fakePortal.goToHome() + await fakePortal.goToHome(username, password) if (!await fakePortal.isLoaded()) { throw new Error('Failed to load fakePortal') } diff --git a/selenium/test/oauth/with-idp-initiated/token-expires.js b/selenium/test/oauth/with-idp-initiated/token-expires.js index 50094027d91d..c1136526837a 100644 --- a/selenium/test/oauth/with-idp-initiated/token-expires.js +++ b/selenium/test/oauth/with-idp-initiated/token-expires.js @@ -12,16 +12,22 @@ describe('Once user logs in with its own token', function () { let homePage let fakePortal let captureScreen + let username + let password + this.timeout(17000) before(async function () { driver = buildDriver() + username = process.env.MGT_CLIENT_ID_FOR_IDP_INITIATED || 'rabbit_idp_user' + password = process.env.MGT_CLIENT_SECRET_FOR_IDP_INITIATED || 'rabbit_idp_user' + fakePortal = new FakePortalPage(driver) homePage = new SSOHomePage(driver) overview = new OverviewPage(driver) captureScreen = captureScreensFor(driver, __filename) - await fakePortal.goToHome() + await fakePortal.goToHome(username, password) if (!await fakePortal.isLoaded()) { throw new Error('Failed to load fakePortal') } diff --git a/selenium/test/oauth/with-idp-initiated/unauthorized.js b/selenium/test/oauth/with-idp-initiated/unauthorized.js index f26daba01669..48ead8974f73 100644 --- a/selenium/test/oauth/with-idp-initiated/unauthorized.js +++ b/selenium/test/oauth/with-idp-initiated/unauthorized.js @@ -7,16 +7,19 @@ const SSOHomePage = require('../../pageobjects/SSOHomePage') const FakePortalPage = require('../../pageobjects/FakePortalPage') describe('A user who accesses the /login URL with a token without scopes for the management UI', function () { - let overview let captureScreen - let token + let username + let password before(async function () { driver = buildDriver() + username = process.env.MGT_UNAUTHORIZED_CLIENT_ID_FOR_IDP_INITIATED || 'producer' + password = process.env.MGT_UNAUTHORIZED_CLIENT_SECRET_FOR_IDP_INITIATED || 'producer_secret' + captureScreen = captureScreensFor(driver, __filename) fakePortal = new FakePortalPage(driver) homePage = new SSOHomePage(driver) - await fakePortal.goToHome('producer', 'producer_secret') + await fakePortal.goToHome(username, password) if (!await fakePortal.isLoaded()) { throw new Error('Failed to load fakePortal') } diff --git a/selenium/test/pageobjects/SpringLoginPage.js b/selenium/test/pageobjects/SpringLoginPage.js new file mode 100644 index 000000000000..29dd896da58a --- /dev/null +++ b/selenium/test/pageobjects/SpringLoginPage.js @@ -0,0 +1,21 @@ +const { By, Key, until, Builder } = require('selenium-webdriver') + +const BasePage = require('./BasePage') + +const FORM = By.css('form.login-form') +const USERNAME = By.css('input[name="username"]') +const PASSWORD = By.css('input[name="password"]') + +module.exports = class SpringLoginPage extends BasePage { + async isLoaded () { + return this.waitForDisplayed(FORM) + } + + async login (username, password) { + await this.isLoaded() + + await this.sendKeys(USERNAME, username) + await this.sendKeys(PASSWORD, password) + return this.submit(FORM) + } +} diff --git a/selenium/test/utils.js b/selenium/test/utils.js index c862b290cc04..84ec7339a1f7 100644 --- a/selenium/test/utils.js +++ b/selenium/test/utils.js @@ -8,6 +8,7 @@ require('chromedriver') var chrome = require("selenium-webdriver/chrome"); const UAALoginPage = require('./pageobjects/UAALoginPage') const KeycloakLoginPage = require('./pageobjects/KeycloakLoginPage') +const SpringLoginPage = require('./pageobjects/SpringLoginPage') const assert = require('assert') const runLocal = String(process.env.RUN_LOCAL).toLowerCase() != 'false' @@ -215,6 +216,8 @@ module.exports = { preferredIdp = "uaa" } else if (process.env.PROFILES.includes("keycloak")) { preferredIdp = "keycloak" + } else if (process.env.PROFILES.includes("spring")) { + preferredIdp = "spring" } else { throw new Error("Missing uaa or keycloak profiles") } @@ -222,6 +225,7 @@ module.exports = { switch(preferredIdp) { case "uaa": return new UAALoginPage(d) case "keycloak": return new KeycloakLoginPage(d) + case "spring": return new SpringLoginPage(d) default: new Error("Unsupported ipd " + preferredIdp) } }, @@ -236,17 +240,21 @@ module.exports = { } }, - tokenFor: (client_id, client_secret, url = uaaUrl) => { + tokenFor: (client_id, client_secret, url = uaaUrl, scope) => { const req = new XMLHttpRequest() - const params = 'client_id=' + client_id + + let params = 'client_id=' + client_id + '&client_secret=' + client_secret + '&grant_type=client_credentials' + - '&token_format=jwt' + +// '&token_format=' + token_format + '&response_type=token' + if (scope != undefined && scope != "") { + params = params + '&scope=' + scope + } req.open('POST', url, false) req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') req.setRequestHeader('Accept', 'application/json') + req.setRequestHeader("Authorization", "Basic " + btoa(client_id+":"+client_secret)); req.send(params) if (req.status == 200) return JSON.parse(req.responseText).access_token else {