Merge pull request #11668 from JimMoen/saml-login-redirect

fix: saml login acs redirect to dashboard overview
This commit is contained in:
JimMoen 2023-09-25 18:11:03 +08:00 committed by GitHub
commit 7d58f6c61e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 80 additions and 57 deletions

View File

@ -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())),

View File

@ -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)).

View File

@ -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",