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
|
backend/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([sso_parameters/1, login_reply/2]).
|
-export([sso_parameters/1, login_meta/3]).
|
||||||
|
|
||||||
-define(REDIRECT, 'REDIRECT').
|
-define(REDIRECT, 'REDIRECT').
|
||||||
-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD').
|
-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD').
|
||||||
|
@ -151,7 +151,7 @@ running(get, _Request) ->
|
||||||
maps:values(SSO)
|
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
|
case emqx_dashboard_sso_manager:lookup_state(Backend) of
|
||||||
undefined ->
|
undefined ->
|
||||||
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
{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
|
case emqx_dashboard_sso:login(provider(Backend), Request, State) of
|
||||||
{ok, Role, Token} ->
|
{ok, Role, Token} ->
|
||||||
?SLOG(info, #{msg => "dashboard_sso_login_successful", request => Request}),
|
?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} ->
|
{redirect, Redirect} ->
|
||||||
?SLOG(info, #{msg => "dashboard_sso_login_redirect", request => Request}),
|
?SLOG(info, #{msg => "dashboard_sso_login_redirect", request => Request}),
|
||||||
Redirect;
|
Redirect;
|
||||||
|
@ -266,8 +267,9 @@ to_json(Data) ->
|
||||||
end
|
end
|
||||||
).
|
).
|
||||||
|
|
||||||
login_reply(Role, Token) ->
|
login_meta(Username, Role, Token) ->
|
||||||
#{
|
#{
|
||||||
|
username => Username,
|
||||||
role => Role,
|
role => Role,
|
||||||
token => Token,
|
token => Token,
|
||||||
version => iolist_to_binary(proplists:get_value(version, emqx_sys:info())),
|
version => iolist_to_binary(proplists:get_value(version, emqx_sys:info())),
|
||||||
|
|
|
@ -29,6 +29,9 @@
|
||||||
|
|
||||||
-dialyzer({nowarn_function, do_create/1}).
|
-dialyzer({nowarn_function, do_create/1}).
|
||||||
|
|
||||||
|
-define(RESPHEADERS, #{<<"Cache-Control">> => <<"no-cache">>, <<"Pragma">> => <<"no-cache">>}).
|
||||||
|
-define(REDIRECT_BODY, <<"Redirecting...">>).
|
||||||
|
|
||||||
-define(DIR, <<"saml_sp_certs">>).
|
-define(DIR, <<"saml_sp_certs">>).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -103,7 +106,49 @@ create(#{sp_sign_request := true} = Config) ->
|
||||||
{error, Msg}
|
{error, Msg}
|
||||||
end;
|
end;
|
||||||
create(#{sp_sign_request := false} = Config) ->
|
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(
|
do_create(
|
||||||
#{
|
#{
|
||||||
|
@ -145,46 +190,6 @@ do_create(
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
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) ->
|
do_validate_assertion(SP, DuplicateFun, Body) ->
|
||||||
PostVals = cow_qs:parse_qs(Body),
|
PostVals = cow_qs:parse_qs(Body),
|
||||||
SAMLEncoding = proplists:get_value(<<"SAMLEncoding">>, PostVals),
|
SAMLEncoding = proplists:get_value(<<"SAMLEncoding">>, PostVals),
|
||||||
|
@ -200,8 +205,17 @@ do_validate_assertion(SP, DuplicateFun, Body) ->
|
||||||
end
|
end
|
||||||
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) ->
|
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)})
|
error({missing_key, lists:flatten(KeyPath)})
|
||||||
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).
|
|
||||||
|
|
||||||
%% TODO: unify with emqx_dashboard_sso_manager:ensure_user_exists/1
|
%% TODO: unify with emqx_dashboard_sso_manager:ensure_user_exists/1
|
||||||
ensure_user_exists(Username) ->
|
ensure_user_exists(Username) ->
|
||||||
case emqx_dashboard_admin:lookup_user(saml, Username) of
|
case emqx_dashboard_admin:lookup_user(saml, Username) of
|
||||||
|
@ -238,3 +243,19 @@ ensure_user_exists(Username) ->
|
||||||
Error
|
Error
|
||||||
end
|
end
|
||||||
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">>}};
|
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||||
State ->
|
State ->
|
||||||
case (provider(saml)):callback(Req, State) of
|
case (provider(saml)):callback(Req, State) of
|
||||||
{ok, Role, Token} ->
|
{redirect, Redirect} ->
|
||||||
{200, emqx_dashboard_sso_api:login_reply(Role, Token)};
|
Redirect;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(info, #{
|
?SLOG(info, #{
|
||||||
msg => "dashboard_saml_sso_login_failed",
|
msg => "dashboard_saml_sso_login_failed",
|
||||||
|
|
Loading…
Reference in New Issue