chore: split out sso_saml_api module

This commit is contained in:
JianBo He 2023-09-22 16:31:55 +08:00 committed by JimMoen
parent df94426ee3
commit 9181ec844f
No known key found for this signature in database
GPG Key ID: 87A520B4F76BA86D
3 changed files with 155 additions and 87 deletions

View File

@ -30,12 +30,10 @@
running/2, running/2,
login/2, login/2,
sso/2, sso/2,
backend/2, backend/2
sp_saml_metadata/2,
sp_saml_callback/2
]). ]).
-export([sso_parameters/1]). -export([sso_parameters/1, login_reply/2]).
-define(REDIRECT, 'REDIRECT'). -define(REDIRECT, 'REDIRECT').
-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD'). -define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD').
@ -53,10 +51,7 @@ paths() ->
"/sso", "/sso",
"/sso/:backend", "/sso/:backend",
"/sso/running", "/sso/running",
"/sso/login/:backend", "/sso/login/:backend"
"/sso/saml/acs",
"/sso/saml/metadata"
%% "/sso_saml/logout"
]. ].
schema("/sso/running") -> schema("/sso/running") ->
@ -134,42 +129,8 @@ schema("/sso/:backend") ->
404 => response_schema(404) 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) -> fields(backend_status) ->
emqx_dashboard_sso_schema:common_backend_schema(emqx_dashboard_sso:types()). 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}), ?SLOG(info, #{msg => "Delete SSO backend", backend => Backend}),
handle_backend_update_result(emqx_dashboard_sso_manager:delete(Backend), undefined). 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) -> sso_parameters(Params) ->
backend_name_as_arg(query, [local], <<"local">>) ++ Params. backend_name_as_arg(query, [local], <<"local">>) ++ Params.
@ -285,18 +218,6 @@ backend_union() ->
login_union() -> login_union() ->
hoconsc:union([emqx_dashboard_sso:login_ref(Mod) || Mod <- emqx_dashboard_sso:modules()]). 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_in_path() ->
backend_name_as_arg(path, [], <<"ldap">>). backend_name_as_arg(path, [], <<"ldap">>).

View File

@ -151,17 +151,32 @@ login(_Req, #{sp := SP, idp_meta := #esaml_idp_metadata{login_location = IDP}} =
end, end,
{redirect, RedirectFun}. {redirect, RedirectFun}.
callback(Req, #{sp := SP} = _State) -> callback(_Req = #{body := Body}, #{sp := SP} = _State) ->
case esaml_cowboy:validate_assertion(SP, fun esaml_util:check_dupe_ets/2, Req) of case do_validate_assertion(SP, fun esaml_util:check_dupe_ets/2, Body) of
{ok, Assertion, _RelayState, _Req2} -> {ok, Assertion, _RelayState} ->
Subject = Assertion#esaml_assertion.subject, Subject = Assertion#esaml_assertion.subject,
Username = iolist_to_binary(Subject#esaml_subject.name), Username = iolist_to_binary(Subject#esaml_subject.name),
ensure_user_exists(Username); ensure_user_exists(Username);
{error, Reason0, _Req2} -> {error, Reason0} ->
Reason = [ Reason = [
"Access denied, assertion failed validation:\n", io_lib:format("~p\n", [Reason0]) "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. end.
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -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">>
}
}
}
}.