Merge pull request #11668 from JimMoen/saml-login-redirect
fix: saml login acs redirect to dashboard overview
This commit is contained in:
commit
7d58f6c61e
|
@ -33,7 +33,7 @@
|
|||
backend/2
|
||||
]).
|
||||
|
||||
-export([sso_parameters/1, login_reply/2]).
|
||||
-export([sso_parameters/1, login_meta/3]).
|
||||
|
||||
-define(REDIRECT, 'REDIRECT').
|
||||
-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD').
|
||||
|
@ -151,7 +151,7 @@ running(get, _Request) ->
|
|||
maps:values(SSO)
|
||||
)}.
|
||||
|
||||
login(post, #{bindings := #{backend := Backend}} = Request) ->
|
||||
login(post, #{bindings := #{backend := Backend}, body := Body} = Request) ->
|
||||
case emqx_dashboard_sso_manager:lookup_state(Backend) of
|
||||
undefined ->
|
||||
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||
|
@ -159,7 +159,8 @@ login(post, #{bindings := #{backend := Backend}} = Request) ->
|
|||
case emqx_dashboard_sso:login(provider(Backend), Request, State) of
|
||||
{ok, Role, Token} ->
|
||||
?SLOG(info, #{msg => "dashboard_sso_login_successful", request => Request}),
|
||||
{200, login_reply(Role, Token)};
|
||||
Username = maps:get(<<"username">>, Body),
|
||||
{200, login_meta(Username, Role, Token)};
|
||||
{redirect, Redirect} ->
|
||||
?SLOG(info, #{msg => "dashboard_sso_login_redirect", request => Request}),
|
||||
Redirect;
|
||||
|
@ -266,8 +267,9 @@ to_json(Data) ->
|
|||
end
|
||||
).
|
||||
|
||||
login_reply(Role, Token) ->
|
||||
login_meta(Username, Role, Token) ->
|
||||
#{
|
||||
username => Username,
|
||||
role => Role,
|
||||
token => Token,
|
||||
version => iolist_to_binary(proplists:get_value(version, emqx_sys:info())),
|
||||
|
|
|
@ -29,6 +29,9 @@
|
|||
|
||||
-dialyzer({nowarn_function, do_create/1}).
|
||||
|
||||
-define(RESPHEADERS, #{<<"Cache-Control">> => <<"no-cache">>, <<"Pragma">> => <<"no-cache">>}).
|
||||
-define(REDIRECT_BODY, <<"Redirecting...">>).
|
||||
|
||||
-define(DIR, <<"saml_sp_certs">>).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -103,7 +106,49 @@ create(#{sp_sign_request := true} = Config) ->
|
|||
{error, Msg}
|
||||
end;
|
||||
create(#{sp_sign_request := false} = Config) ->
|
||||
do_create(Config#{key => undefined, certificate => undefined}).
|
||||
do_create(Config#{sp_private_key => undefined, sp_public_key => undefined}).
|
||||
|
||||
update(Config0, State) ->
|
||||
destroy(State),
|
||||
create(Config0).
|
||||
|
||||
destroy(_State) ->
|
||||
_ = file:del_dir_r(emqx_tls_lib:pem_dir(?DIR)),
|
||||
_ = application:stop(esaml),
|
||||
ok.
|
||||
|
||||
login(
|
||||
#{headers := Headers} = _Req,
|
||||
#{sp := SP, idp_meta := #esaml_idp_metadata{login_location = IDP}} = _State
|
||||
) ->
|
||||
SignedXml = esaml_sp:generate_authn_request(IDP, SP),
|
||||
Target = esaml_binding:encode_http_redirect(IDP, SignedXml, <<>>),
|
||||
Redirect =
|
||||
case is_msie(Headers) of
|
||||
true ->
|
||||
Html = esaml_binding:encode_http_post(IDP, SignedXml, <<>>),
|
||||
{200, ?RESPHEADERS, Html};
|
||||
false ->
|
||||
{302, ?RESPHEADERS#{<<"Location">> => Target}, ?REDIRECT_BODY}
|
||||
end,
|
||||
{redirect, Redirect}.
|
||||
|
||||
callback(_Req = #{body := Body}, #{sp := SP, dashboard_addr := DashboardAddr} = _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),
|
||||
gen_redirect_response(DashboardAddr, Username);
|
||||
{error, Reason0} ->
|
||||
Reason = [
|
||||
"Access denied, assertion failed validation:\n", io_lib:format("~p\n", [Reason0])
|
||||
],
|
||||
{error, iolist_to_binary(Reason)}
|
||||
end.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
do_create(
|
||||
#{
|
||||
|
@ -145,46 +190,6 @@ do_create(
|
|||
{error, Reason}
|
||||
end.
|
||||
|
||||
update(Config0, State) ->
|
||||
destroy(State),
|
||||
create(Config0).
|
||||
|
||||
destroy(_State) ->
|
||||
_ = file:del_dir_r(emqx_tls_lib:pem_dir(?DIR)),
|
||||
_ = application:stop(esaml),
|
||||
ok.
|
||||
|
||||
login(
|
||||
#{headers := Headers} = _Req,
|
||||
#{sp := SP, idp_meta := #esaml_idp_metadata{login_location = IDP}} = _State
|
||||
) ->
|
||||
SignedXml = esaml_sp:generate_authn_request(IDP, SP),
|
||||
Target = esaml_binding:encode_http_redirect(IDP, SignedXml, <<>>),
|
||||
RespHeaders = #{<<"Cache-Control">> => <<"no-cache">>, <<"Pragma">> => <<"no-cache">>},
|
||||
Redirect =
|
||||
case is_msie(Headers) of
|
||||
true ->
|
||||
Html = esaml_binding:encode_http_post(IDP, SignedXml, <<>>),
|
||||
{200, RespHeaders, Html};
|
||||
false ->
|
||||
RespHeaders1 = RespHeaders#{<<"Location">> => Target},
|
||||
{302, RespHeaders1, <<"Redirecting...">>}
|
||||
end,
|
||||
{redirect, Redirect}.
|
||||
|
||||
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} ->
|
||||
Reason = [
|
||||
"Access denied, assertion failed validation:\n", io_lib:format("~p\n", [Reason0])
|
||||
],
|
||||
{error, iolist_to_binary(Reason)}
|
||||
end.
|
||||
|
||||
do_validate_assertion(SP, DuplicateFun, Body) ->
|
||||
PostVals = cow_qs:parse_qs(Body),
|
||||
SAMLEncoding = proplists:get_value(<<"SAMLEncoding">>, PostVals),
|
||||
|
@ -200,8 +205,17 @@ do_validate_assertion(SP, DuplicateFun, Body) ->
|
|||
end
|
||||
end.
|
||||
|
||||
gen_redirect_response(DashboardAddr, Username) ->
|
||||
case ensure_user_exists(Username) of
|
||||
{ok, Role, Token} ->
|
||||
Target = login_redirect_target(DashboardAddr, Username, Role, Token),
|
||||
{redirect, {302, ?RESPHEADERS#{<<"Location">> => Target}, ?REDIRECT_BODY}};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%% Helpers functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
ensure_cert_and_key(#{sp_public_key := Cert, sp_private_key := Key} = Config) ->
|
||||
|
@ -216,15 +230,6 @@ ensure_cert_and_key(#{sp_public_key := Cert, sp_private_key := Key} = Config) ->
|
|||
error({missing_key, lists:flatten(KeyPath)})
|
||||
end.
|
||||
|
||||
maybe_load_cert_or_key(undefined, _) ->
|
||||
undefined;
|
||||
maybe_load_cert_or_key(Path, Func) ->
|
||||
Func(Path).
|
||||
|
||||
is_msie(Headers) ->
|
||||
UA = maps:get(<<"user-agent">>, Headers, <<"">>),
|
||||
not (binary:match(UA, <<"MSIE">>) =:= nomatch).
|
||||
|
||||
%% TODO: unify with emqx_dashboard_sso_manager:ensure_user_exists/1
|
||||
ensure_user_exists(Username) ->
|
||||
case emqx_dashboard_admin:lookup_user(saml, Username) of
|
||||
|
@ -238,3 +243,19 @@ ensure_user_exists(Username) ->
|
|||
Error
|
||||
end
|
||||
end.
|
||||
|
||||
maybe_load_cert_or_key(undefined, _) ->
|
||||
undefined;
|
||||
maybe_load_cert_or_key(Path, Func) ->
|
||||
Func(Path).
|
||||
|
||||
is_msie(Headers) ->
|
||||
UA = maps:get(<<"user-agent">>, Headers, <<"">>),
|
||||
not (binary:match(UA, <<"MSIE">>) =:= nomatch).
|
||||
|
||||
login_redirect_target(DashboardAddr, Username, Role, Token) ->
|
||||
LoginMeta = emqx_dashboard_sso_api:login_meta(Username, Role, Token),
|
||||
<<DashboardAddr/binary, "/?login_meta=", (base64_login_meta(LoginMeta))/binary>>.
|
||||
|
||||
base64_login_meta(LoginMeta) ->
|
||||
base64:encode(emqx_utils_json:encode(LoginMeta)).
|
||||
|
|
|
@ -96,8 +96,8 @@ sp_saml_callback(post, Req) ->
|
|||
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||
State ->
|
||||
case (provider(saml)):callback(Req, State) of
|
||||
{ok, Role, Token} ->
|
||||
{200, emqx_dashboard_sso_api:login_reply(Role, Token)};
|
||||
{redirect, Redirect} ->
|
||||
Redirect;
|
||||
{error, Reason} ->
|
||||
?SLOG(info, #{
|
||||
msg => "dashboard_saml_sso_login_failed",
|
||||
|
|
Loading…
Reference in New Issue