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