From 9181ec844f0ba57fcbac5a238eef21492ff22fab Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 22 Sep 2023 16:31:55 +0800 Subject: [PATCH] chore: split out sso_saml_api module --- .../src/emqx_dashboard_sso_api.erl | 85 +---------- .../src/emqx_dashboard_sso_saml.erl | 25 +++- .../src/emqx_dashboard_sso_saml_api.erl | 132 ++++++++++++++++++ 3 files changed, 155 insertions(+), 87 deletions(-) create mode 100644 apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl index 91373d93b..f2cd02ecb 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -30,12 +30,10 @@ running/2, login/2, sso/2, - backend/2, - sp_saml_metadata/2, - sp_saml_callback/2 + backend/2 ]). --export([sso_parameters/1]). +-export([sso_parameters/1, login_reply/2]). -define(REDIRECT, 'REDIRECT'). -define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD'). @@ -53,10 +51,7 @@ paths() -> "/sso", "/sso/:backend", "/sso/running", - "/sso/login/:backend", - "/sso/saml/acs", - "/sso/saml/metadata" - %% "/sso_saml/logout" + "/sso/login/:backend" ]. schema("/sso/running") -> @@ -134,42 +129,8 @@ schema("/sso/:backend") -> 404 => response_schema(404) } } - }; -%% Handles HTTP-POST bound assertions coming back from the IDP. -schema("/sso/saml/acs") -> - #{ - 'operationId' => sp_saml_callback, - post => #{ - tags => [?TAGS], - desc => ?DESC(saml_sso_acs), - %% 'requestbody' => saml_response(), - %% SAMLResponse and RelayState - %% should return 302 to redirect to dashboard - responses => #{ - 302 => response_schema(302), - 401 => response_schema(401), - 404 => response_schema(404) - }, - security => [] - } - }; -schema("/sso/saml/metadata") -> - #{ - 'operationId' => sp_saml_metadata, - get => #{ - tags => [?TAGS], - desc => ?DESC(sp_saml_metadata), - 'requestbody' => saml_metadata_response(), - responses => #{ - 200 => emqx_dashboard_api:fields([token, version, license]), - 404 => response_schema(404) - } - } }. -%% TODO: -%% schema("/sso_saml/logout") -> - fields(backend_status) -> emqx_dashboard_sso_schema:common_backend_schema(emqx_dashboard_sso:types()). @@ -237,34 +198,6 @@ backend(delete, #{bindings := #{backend := Backend}}) -> ?SLOG(info, #{msg => "Delete SSO backend", backend => Backend}), handle_backend_update_result(emqx_dashboard_sso_manager:delete(Backend), undefined). -sp_saml_metadata(get, _Req) -> - case emqx_dashboard_sso_manager:lookup_state(saml) of - undefined -> - {404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}}; - #{sp := SP} = _State -> - SignedXml = esaml_sp:generate_metadata(SP), - Metadata = xmerl:export([SignedXml], xmerl_xml), - {200, [{<<"Content-Type">>, <<"text/xml">>}], Metadata} - end. - -sp_saml_callback(post, Req) -> - case emqx_dashboard_sso_manager:lookup_state(saml) of - undefined -> - {404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}}; - State -> - case (provider(saml)):callback(Req, State) of - {ok, Token} -> - {200, [{<<"Content-Type">>, <<"text/html">>}], login_reply(Token)}; - {error, Reason} -> - ?SLOG(info, #{ - msg => "dashboard_saml_sso_login_failed", - request => Req, - reason => Reason - }), - {403, #{code => <<"UNAUTHORIZED">>, message => Reason}} - end - end. - sso_parameters(Params) -> backend_name_as_arg(query, [local], <<"local">>) ++ Params. @@ -285,18 +218,6 @@ backend_union() -> login_union() -> hoconsc:union([emqx_dashboard_sso:login_ref(Mod) || Mod <- emqx_dashboard_sso:modules()]). -saml_metadata_response() -> - #{ - 'content' => #{ - 'application/xml' => #{ - schema => #{ - type => <<"string">>, - format => <<"binary">> - } - } - } - }. - backend_name_in_path() -> backend_name_as_arg(path, [], <<"ldap">>). diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl index 4d172dcdb..9f2b5cc48 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl @@ -151,17 +151,32 @@ login(_Req, #{sp := SP, idp_meta := #esaml_idp_metadata{login_location = IDP}} = end, {redirect, RedirectFun}. -callback(Req, #{sp := SP} = _State) -> - case esaml_cowboy:validate_assertion(SP, fun esaml_util:check_dupe_ets/2, Req) of - {ok, Assertion, _RelayState, _Req2} -> +callback(_Req = #{body := Body}, #{sp := SP} = _State) -> + case do_validate_assertion(SP, fun esaml_util:check_dupe_ets/2, Body) of + {ok, Assertion, _RelayState} -> Subject = Assertion#esaml_assertion.subject, Username = iolist_to_binary(Subject#esaml_subject.name), ensure_user_exists(Username); - {error, Reason0, _Req2} -> + {error, Reason0} -> Reason = [ "Access denied, assertion failed validation:\n", io_lib:format("~p\n", [Reason0]) ], - {error, Reason} + {error, iolist_to_binary(Reason)} + end. + +do_validate_assertion(SP, DuplicateFun, Body) -> + PostVals = cow_qs:parse_qs(Body), + SAMLEncoding = proplists:get_value(<<"SAMLEncoding">>, PostVals), + SAMLResponse = proplists:get_value(<<"SAMLResponse">>, PostVals), + RelayState = proplists:get_value(<<"RelayState">>, PostVals), + case (catch esaml_binding:decode_response(SAMLEncoding, SAMLResponse)) of + {'EXIT', Reason} -> + {error, {bad_decode, Reason}}; + Xml -> + case esaml_sp:validate_assertion(Xml, DuplicateFun, SP) of + {ok, A} -> {ok, A, RelayState}; + {error, E} -> {error, E} + end end. %%------------------------------------------------------------------------------ diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl new file mode 100644 index 000000000..492012153 --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl @@ -0,0 +1,132 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_dashboard_sso_saml_api). + +-behaviour(minirest_api). + +-include_lib("hocon/include/hoconsc.hrl"). +-include_lib("emqx/include/logger.hrl"). + +-import(hoconsc, [ + mk/2, + array/1, + enum/1, + ref/1 +]). + +-import(emqx_dashboard_sso, [provider/1]). + +-export([ + api_spec/0, + paths/0, + schema/1, + namespace/0 +]). + +-export([ + sp_saml_metadata/2, + sp_saml_callback/2 +]). + +-define(REDIRECT, 'REDIRECT'). +-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD'). +-define(BACKEND_NOT_FOUND, 'BACKEND_NOT_FOUND'). +-define(TAGS, <<"Dashboard Single Sign-On">>). + +namespace() -> "dashboard_sso". + +api_spec() -> + emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false, translate_body => false}). + +paths() -> + [ + "/sso/saml/acs", + "/sso/saml/metadata" + ]. + +%% Handles HTTP-POST bound assertions coming back from the IDP. +schema("/sso/saml/acs") -> + #{ + 'operationId' => sp_saml_callback, + post => #{ + tags => [?TAGS], + desc => ?DESC(saml_sso_acs), + %% 'requestbody' => urlencoded_request_body(), + responses => #{ + 302 => response_schema(302), + 401 => response_schema(401), + 404 => response_schema(404) + }, + security => [] + } + }; +schema("/sso/saml/metadata") -> + #{ + 'operationId' => sp_saml_metadata, + get => #{ + tags => [?TAGS], + desc => ?DESC(sp_saml_metadata), + 'requestbody' => saml_metadata_response(), + responses => #{ + 200 => emqx_dashboard_api:fields([token, version, license]), + 404 => response_schema(404) + } + } + }. + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +sp_saml_metadata(get, _Req) -> + case emqx_dashboard_sso_manager:lookup_state(saml) of + undefined -> + {404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}}; + #{sp := SP} = _State -> + SignedXml = esaml_sp:generate_metadata(SP), + Metadata = xmerl:export([SignedXml], xmerl_xml), + {200, [{<<"Content-Type">>, <<"text/xml">>}], Metadata} + end. + +sp_saml_callback(post, Req) -> + case emqx_dashboard_sso_manager:lookup_state(saml) of + undefined -> + {404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}}; + State -> + case (provider(saml)):callback(Req, State) of + {ok, Token} -> + {200, emqx_dashboard_sso_api:login_reply(Token)}; + {error, Reason} -> + ?SLOG(info, #{ + msg => "dashboard_saml_sso_login_failed", + request => Req, + reason => Reason + }), + {403, #{code => <<"UNAUTHORIZED">>, message => Reason}} + end + end. + +%%-------------------------------------------------------------------- +%% internal +%%-------------------------------------------------------------------- + +response_schema(302) -> + emqx_dashboard_swagger:error_codes([?REDIRECT], ?DESC(redirect)); +response_schema(401) -> + emqx_dashboard_swagger:error_codes([?BAD_USERNAME_OR_PWD], ?DESC(login_failed401)); +response_schema(404) -> + emqx_dashboard_swagger:error_codes([?BACKEND_NOT_FOUND], ?DESC(backend_not_found)). + +saml_metadata_response() -> + #{ + 'content' => #{ + 'application/xml' => #{ + schema => #{ + type => <<"string">>, + format => <<"binary">> + } + } + } + }.