chore: split out sso_saml_api module
This commit is contained in:
parent
df94426ee3
commit
9181ec844f
|
@ -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">>).
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
|
|
|
@ -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">>
|
||||
}
|
||||
}
|
||||
}
|
||||
}.
|
Loading…
Reference in New Issue