Merge remote-tracking branch 'origin/release-572' into release-57
This commit is contained in:
commit
b6e7d7566d
|
@ -284,28 +284,24 @@ parse_connect(FrameBin, StrictMode) ->
|
||||||
end,
|
end,
|
||||||
parse_connect2(ProtoName, Rest, StrictMode).
|
parse_connect2(ProtoName, Rest, StrictMode).
|
||||||
|
|
||||||
% Note: return malformed if reserved flag is not 0.
|
|
||||||
parse_connect2(
|
parse_connect2(
|
||||||
ProtoName,
|
ProtoName,
|
||||||
<<BridgeTag:4, ProtoVer:4, UsernameFlag:1, PasswordFlag:1, WillRetainB:1, WillQoS:2,
|
<<BridgeTag:4, ProtoVer:4, UsernameFlagB:1, PasswordFlagB:1, WillRetainB:1, WillQoS:2,
|
||||||
WillFlagB:1, CleanStart:1, Reserved:1, KeepAlive:16/big, Rest2/binary>>,
|
WillFlagB:1, CleanStart:1, Reserved:1, KeepAlive:16/big, Rest2/binary>>,
|
||||||
StrictMode
|
StrictMode
|
||||||
) ->
|
) ->
|
||||||
case Reserved of
|
_ = validate_connect_reserved(Reserved),
|
||||||
0 -> ok;
|
_ = validate_connect_will(
|
||||||
1 -> ?PARSE_ERR(reserved_connect_flag)
|
|
||||||
end,
|
|
||||||
WillFlag = bool(WillFlagB),
|
WillFlag = bool(WillFlagB),
|
||||||
WillRetain = bool(WillRetainB),
|
WillRetain = bool(WillRetainB),
|
||||||
case WillFlag of
|
WillQoS
|
||||||
%% MQTT-v3.1.1-[MQTT-3.1.2-13], MQTT-v5.0-[MQTT-3.1.2-11]
|
),
|
||||||
false when WillQoS > 0 -> ?PARSE_ERR(invalid_will_qos);
|
_ = validate_connect_password_flag(
|
||||||
%% MQTT-v3.1.1-[MQTT-3.1.2-14], MQTT-v5.0-[MQTT-3.1.2-12]
|
StrictMode,
|
||||||
true when WillQoS > 2 -> ?PARSE_ERR(invalid_will_qos);
|
ProtoVer,
|
||||||
%% MQTT-v3.1.1-[MQTT-3.1.2-15], MQTT-v5.0-[MQTT-3.1.2-13]
|
UsernameFlag = bool(UsernameFlagB),
|
||||||
false when WillRetain -> ?PARSE_ERR(invalid_will_retain);
|
PasswordFlag = bool(PasswordFlagB)
|
||||||
_ -> ok
|
),
|
||||||
end,
|
|
||||||
{Properties, Rest3} = parse_properties(Rest2, ProtoVer, StrictMode),
|
{Properties, Rest3} = parse_properties(Rest2, ProtoVer, StrictMode),
|
||||||
{ClientId, Rest4} = parse_utf8_string_with_cause(Rest3, StrictMode, invalid_clientid),
|
{ClientId, Rest4} = parse_utf8_string_with_cause(Rest3, StrictMode, invalid_clientid),
|
||||||
ConnPacket = #mqtt_packet_connect{
|
ConnPacket = #mqtt_packet_connect{
|
||||||
|
@ -328,14 +324,14 @@ parse_connect2(
|
||||||
fun(Bin) ->
|
fun(Bin) ->
|
||||||
parse_utf8_string_with_cause(Bin, StrictMode, invalid_username)
|
parse_utf8_string_with_cause(Bin, StrictMode, invalid_username)
|
||||||
end,
|
end,
|
||||||
bool(UsernameFlag)
|
UsernameFlag
|
||||||
),
|
),
|
||||||
{Password, Rest7} = parse_optional(
|
{Password, Rest7} = parse_optional(
|
||||||
Rest6,
|
Rest6,
|
||||||
fun(Bin) ->
|
fun(Bin) ->
|
||||||
parse_utf8_string_with_cause(Bin, StrictMode, invalid_password)
|
parse_utf8_string_with_cause(Bin, StrictMode, invalid_password)
|
||||||
end,
|
end,
|
||||||
bool(PasswordFlag)
|
PasswordFlag
|
||||||
),
|
),
|
||||||
case Rest7 of
|
case Rest7 of
|
||||||
<<>> ->
|
<<>> ->
|
||||||
|
@ -1133,6 +1129,32 @@ validate_subqos([3 | _]) -> ?PARSE_ERR(bad_subqos);
|
||||||
validate_subqos([_ | T]) -> validate_subqos(T);
|
validate_subqos([_ | T]) -> validate_subqos(T);
|
||||||
validate_subqos([]) -> ok.
|
validate_subqos([]) -> ok.
|
||||||
|
|
||||||
|
%% MQTT-v3.1.1-[MQTT-3.1.2-3], MQTT-v5.0-[MQTT-3.1.2-3]
|
||||||
|
validate_connect_reserved(0) -> ok;
|
||||||
|
validate_connect_reserved(1) -> ?PARSE_ERR(reserved_connect_flag).
|
||||||
|
|
||||||
|
%% MQTT-v3.1.1-[MQTT-3.1.2-13], MQTT-v5.0-[MQTT-3.1.2-11]
|
||||||
|
validate_connect_will(false, _, WillQos) when WillQos > 0 -> ?PARSE_ERR(invalid_will_qos);
|
||||||
|
%% MQTT-v3.1.1-[MQTT-3.1.2-14], MQTT-v5.0-[MQTT-3.1.2-12]
|
||||||
|
validate_connect_will(true, _, WillQoS) when WillQoS > 2 -> ?PARSE_ERR(invalid_will_qos);
|
||||||
|
%% MQTT-v3.1.1-[MQTT-3.1.2-15], MQTT-v5.0-[MQTT-3.1.2-13]
|
||||||
|
validate_connect_will(false, WillRetain, _) when WillRetain -> ?PARSE_ERR(invalid_will_retain);
|
||||||
|
validate_connect_will(_, _, _) -> ok.
|
||||||
|
|
||||||
|
%% MQTT-v3.1
|
||||||
|
%% Username flag and password flag are not strongly related
|
||||||
|
%% https://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html#connect
|
||||||
|
validate_connect_password_flag(true, ?MQTT_PROTO_V3, _, _) ->
|
||||||
|
ok;
|
||||||
|
%% MQTT-v3.1.1-[MQTT-3.1.2-22]
|
||||||
|
validate_connect_password_flag(true, ?MQTT_PROTO_V4, UsernameFlag, PasswordFlag) ->
|
||||||
|
%% BUG-FOR-BUG compatible, only check when `strict-mode`
|
||||||
|
UsernameFlag orelse PasswordFlag andalso ?PARSE_ERR(invalid_password_flag);
|
||||||
|
validate_connect_password_flag(true, ?MQTT_PROTO_V5, _, _) ->
|
||||||
|
ok;
|
||||||
|
validate_connect_password_flag(_, _, _, _) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
bool(0) -> false;
|
bool(0) -> false;
|
||||||
bool(1) -> true.
|
bool(1) -> true.
|
||||||
|
|
||||||
|
|
|
@ -706,9 +706,15 @@ t_invalid_clientid(_) ->
|
||||||
).
|
).
|
||||||
|
|
||||||
%% for regression: `password` must be `undefined`
|
%% for regression: `password` must be `undefined`
|
||||||
|
%% BUG-FOR-BUG compatible
|
||||||
t_undefined_password(_) ->
|
t_undefined_password(_) ->
|
||||||
Payload = <<16, 19, 0, 4, 77, 81, 84, 84, 4, 130, 0, 60, 0, 2, 97, 49, 0, 3, 97, 97, 97>>,
|
%% Username Flag = true
|
||||||
{ok, Packet, <<>>, {none, _}} = emqx_frame:parse(Payload),
|
%% Password Flag = false
|
||||||
|
%% Clean Session = true
|
||||||
|
ConnectFlags = <<2#1000:4, 2#0010:4>>,
|
||||||
|
ConnBin =
|
||||||
|
<<16, 17, 0, 4, 77, 81, 84, 84, 4, ConnectFlags/binary, 0, 60, 0, 2, 97, 49, 0, 1, 97>>,
|
||||||
|
{ok, Packet, <<>>, {none, _}} = emqx_frame:parse(ConnBin),
|
||||||
Password = undefined,
|
Password = undefined,
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
#mqtt_packet{
|
#mqtt_packet{
|
||||||
|
@ -732,7 +738,7 @@ t_undefined_password(_) ->
|
||||||
will_props = #{},
|
will_props = #{},
|
||||||
will_topic = undefined,
|
will_topic = undefined,
|
||||||
will_payload = undefined,
|
will_payload = undefined,
|
||||||
username = <<"aaa">>,
|
username = <<"a">>,
|
||||||
password = Password
|
password = Password
|
||||||
},
|
},
|
||||||
payload = undefined
|
payload = undefined
|
||||||
|
@ -741,6 +747,25 @@ t_undefined_password(_) ->
|
||||||
),
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
t_invalid_password_flag(_) ->
|
||||||
|
%% Username Flag = false
|
||||||
|
%% Password Flag = true
|
||||||
|
%% Clean Session = true
|
||||||
|
ConnectFlags = <<2#0100:4, 2#0010:4>>,
|
||||||
|
ConnectBin =
|
||||||
|
<<16, 17, 0, 4, 77, 81, 84, 84, 4, ConnectFlags/binary, 0, 60, 0, 2, 97, 49, 0, 1, 97>>,
|
||||||
|
?assertMatch(
|
||||||
|
{ok, _, _, _},
|
||||||
|
emqx_frame:parse(ConnectBin)
|
||||||
|
),
|
||||||
|
|
||||||
|
StrictModeParseState = emqx_frame:initial_parse_state(#{strict_mode => true}),
|
||||||
|
?assertException(
|
||||||
|
throw,
|
||||||
|
{frame_parse_error, invalid_password_flag},
|
||||||
|
emqx_frame:parse(ConnectBin, StrictModeParseState)
|
||||||
|
).
|
||||||
|
|
||||||
t_invalid_will_retain(_) ->
|
t_invalid_will_retain(_) ->
|
||||||
ConnectFlags = <<2#01100000>>,
|
ConnectFlags = <<2#01100000>>,
|
||||||
ConnectBin =
|
ConnectBin =
|
||||||
|
|
|
@ -4,5 +4,6 @@
|
||||||
{deps, [
|
{deps, [
|
||||||
{emqx_ldap, {path, "../../apps/emqx_ldap"}},
|
{emqx_ldap, {path, "../../apps/emqx_ldap"}},
|
||||||
{emqx_dashboard, {path, "../../apps/emqx_dashboard"}},
|
{emqx_dashboard, {path, "../../apps/emqx_dashboard"}},
|
||||||
{esaml, {git, "https://github.com/emqx/esaml", {tag, "v1.1.3"}}}
|
{esaml, {git, "https://github.com/emqx/esaml", {tag, "v1.1.3"}}},
|
||||||
|
{oidcc, {git, "https://github.com/emqx/oidcc.git", {tag, "v3.2.0-1"}}}
|
||||||
]}.
|
]}.
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
{application, emqx_dashboard_sso, [
|
{application, emqx_dashboard_sso, [
|
||||||
{description, "EMQX Dashboard Single Sign-On"},
|
{description, "EMQX Dashboard Single Sign-On"},
|
||||||
{vsn, "0.1.4"},
|
{vsn, "0.1.5"},
|
||||||
{registered, [emqx_dashboard_sso_sup]},
|
{registered, [emqx_dashboard_sso_sup]},
|
||||||
{applications, [
|
{applications, [
|
||||||
kernel,
|
kernel,
|
||||||
stdlib,
|
stdlib,
|
||||||
emqx_dashboard,
|
emqx_dashboard,
|
||||||
emqx_ldap,
|
emqx_ldap,
|
||||||
esaml
|
esaml,
|
||||||
|
oidcc
|
||||||
]},
|
]},
|
||||||
{mod, {emqx_dashboard_sso_app, []}},
|
{mod, {emqx_dashboard_sso_app, []}},
|
||||||
{env, []},
|
{env, []},
|
||||||
|
|
|
@ -92,7 +92,8 @@ provider(Backend) ->
|
||||||
backends() ->
|
backends() ->
|
||||||
#{
|
#{
|
||||||
ldap => emqx_dashboard_sso_ldap,
|
ldap => emqx_dashboard_sso_ldap,
|
||||||
saml => emqx_dashboard_sso_saml
|
saml => emqx_dashboard_sso_saml,
|
||||||
|
oidc => emqx_dashboard_sso_oidc
|
||||||
}.
|
}.
|
||||||
|
|
||||||
format(Args) ->
|
format(Args) ->
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
backend/2
|
backend/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([sso_parameters/1, login_meta/3]).
|
-export([sso_parameters/1, login_meta/4]).
|
||||||
|
|
||||||
-define(REDIRECT, 'REDIRECT').
|
-define(REDIRECT, 'REDIRECT').
|
||||||
-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD').
|
-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD').
|
||||||
|
@ -168,7 +168,7 @@ login(post, #{bindings := #{backend := Backend}, body := Body} = Request) ->
|
||||||
request => emqx_utils:redact(Request)
|
request => emqx_utils:redact(Request)
|
||||||
}),
|
}),
|
||||||
Username = maps:get(<<"username">>, Body),
|
Username = maps:get(<<"username">>, Body),
|
||||||
{200, login_meta(Username, Role, Token)};
|
{200, login_meta(Username, Role, Token, Backend)};
|
||||||
{redirect, Redirect} ->
|
{redirect, Redirect} ->
|
||||||
?SLOG(info, #{
|
?SLOG(info, #{
|
||||||
msg => "dashboard_sso_login_redirect",
|
msg => "dashboard_sso_login_redirect",
|
||||||
|
@ -286,11 +286,12 @@ to_redacted_json(Data) ->
|
||||||
end
|
end
|
||||||
).
|
).
|
||||||
|
|
||||||
login_meta(Username, Role, Token) ->
|
login_meta(Username, Role, Token, Backend) ->
|
||||||
#{
|
#{
|
||||||
username => Username,
|
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())),
|
||||||
license => #{edition => emqx_release:edition()}
|
license => #{edition => emqx_release:edition()},
|
||||||
|
backend => Backend
|
||||||
}.
|
}.
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
handle_call/3,
|
handle_call/3,
|
||||||
handle_cast/2,
|
handle_cast/2,
|
||||||
handle_info/2,
|
handle_info/2,
|
||||||
|
handle_continue/2,
|
||||||
terminate/2,
|
terminate/2,
|
||||||
code_change/3,
|
code_change/3,
|
||||||
format_status/2
|
format_status/2
|
||||||
|
@ -106,7 +107,14 @@ get_backend_status(Backend, _) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
update(Backend, Config) ->
|
update(Backend, Config) ->
|
||||||
update_config(Backend, {?FUNCTION_NAME, Backend, Config}).
|
UpdateConf =
|
||||||
|
case emqx:get_raw_config(?MOD_KEY_PATH(Backend), #{}) of
|
||||||
|
RawConf when is_map(RawConf) ->
|
||||||
|
emqx_utils:deobfuscate(Config, RawConf);
|
||||||
|
null ->
|
||||||
|
Config
|
||||||
|
end,
|
||||||
|
update_config(Backend, {?FUNCTION_NAME, Backend, UpdateConf}).
|
||||||
delete(Backend) ->
|
delete(Backend) ->
|
||||||
update_config(Backend, {?FUNCTION_NAME, Backend}).
|
update_config(Backend, {?FUNCTION_NAME, Backend}).
|
||||||
|
|
||||||
|
@ -154,8 +162,7 @@ init([]) ->
|
||||||
{read_concurrency, true}
|
{read_concurrency, true}
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
start_backend_services(),
|
{ok, #{}, {continue, start_backend_services}}.
|
||||||
{ok, #{}}.
|
|
||||||
|
|
||||||
handle_call(_Request, _From, State) ->
|
handle_call(_Request, _From, State) ->
|
||||||
Reply = ok,
|
Reply = ok,
|
||||||
|
@ -167,6 +174,12 @@ handle_cast(_Request, State) ->
|
||||||
handle_info(_Info, State) ->
|
handle_info(_Info, State) ->
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_continue(start_backend_services, State) ->
|
||||||
|
start_backend_services(),
|
||||||
|
{noreply, State};
|
||||||
|
handle_continue(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, _State) ->
|
terminate(_Reason, _State) ->
|
||||||
remove_handler(),
|
remove_handler(),
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -0,0 +1,294 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_dashboard_sso_oidc).
|
||||||
|
|
||||||
|
-include_lib("emqx_dashboard/include/emqx_dashboard.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
|
||||||
|
-behaviour(emqx_dashboard_sso).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
namespace/0,
|
||||||
|
fields/1,
|
||||||
|
desc/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
hocon_ref/0,
|
||||||
|
login_ref/0,
|
||||||
|
login/2,
|
||||||
|
create/1,
|
||||||
|
update/2,
|
||||||
|
destroy/1,
|
||||||
|
convert_certs/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(PROVIDER_SVR_NAME, ?MODULE).
|
||||||
|
-define(RESPHEADERS, #{
|
||||||
|
<<"cache-control">> => <<"no-cache">>,
|
||||||
|
<<"pragma">> => <<"no-cache">>,
|
||||||
|
<<"content-type">> => <<"text/plain">>
|
||||||
|
}).
|
||||||
|
-define(REDIRECT_BODY, <<"Redirecting...">>).
|
||||||
|
-define(PKCE_VERIFIER_LEN, 60).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Hocon Schema
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace() ->
|
||||||
|
"sso".
|
||||||
|
|
||||||
|
hocon_ref() ->
|
||||||
|
hoconsc:ref(?MODULE, oidc).
|
||||||
|
|
||||||
|
login_ref() ->
|
||||||
|
hoconsc:ref(?MODULE, login).
|
||||||
|
|
||||||
|
fields(oidc) ->
|
||||||
|
emqx_dashboard_sso_schema:common_backend_schema([oidc]) ++
|
||||||
|
[
|
||||||
|
{issuer,
|
||||||
|
?HOCON(
|
||||||
|
binary(),
|
||||||
|
#{desc => ?DESC(issuer), required => true}
|
||||||
|
)},
|
||||||
|
{clientid,
|
||||||
|
?HOCON(
|
||||||
|
binary(),
|
||||||
|
#{desc => ?DESC(clientid), required => true}
|
||||||
|
)},
|
||||||
|
{secret,
|
||||||
|
emqx_schema_secret:mk(
|
||||||
|
maps:merge(#{desc => ?DESC(secret), required => true}, #{})
|
||||||
|
)},
|
||||||
|
{scopes,
|
||||||
|
?HOCON(
|
||||||
|
?ARRAY(binary()),
|
||||||
|
#{desc => ?DESC(scopes), default => [<<"openid">>]}
|
||||||
|
)},
|
||||||
|
{name_var,
|
||||||
|
?HOCON(
|
||||||
|
binary(),
|
||||||
|
#{desc => ?DESC(name_var), default => <<"${sub}">>}
|
||||||
|
)},
|
||||||
|
{dashboard_addr,
|
||||||
|
?HOCON(binary(), #{
|
||||||
|
desc => ?DESC(dashboard_addr),
|
||||||
|
default => <<"http://127.0.0.1:18083">>
|
||||||
|
})},
|
||||||
|
{session_expiry,
|
||||||
|
?HOCON(emqx_schema:timeout_duration_s(), #{
|
||||||
|
desc => ?DESC(session_expiry),
|
||||||
|
default => <<"30s">>
|
||||||
|
})},
|
||||||
|
{require_pkce,
|
||||||
|
?HOCON(boolean(), #{
|
||||||
|
desc => ?DESC(require_pkce),
|
||||||
|
default => false
|
||||||
|
})},
|
||||||
|
{preferred_auth_methods,
|
||||||
|
?HOCON(
|
||||||
|
?ARRAY(
|
||||||
|
?ENUM([
|
||||||
|
private_key_jwt,
|
||||||
|
client_secret_jwt,
|
||||||
|
client_secret_post,
|
||||||
|
client_secret_basic,
|
||||||
|
none
|
||||||
|
])
|
||||||
|
),
|
||||||
|
#{
|
||||||
|
desc => ?DESC(preferred_auth_methods),
|
||||||
|
default => [
|
||||||
|
client_secret_post,
|
||||||
|
client_secret_basic,
|
||||||
|
none
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{provider,
|
||||||
|
?HOCON(?ENUM([okta, generic]), #{
|
||||||
|
mapping => "oidcc.provider",
|
||||||
|
desc => ?DESC(provider),
|
||||||
|
default => generic
|
||||||
|
})},
|
||||||
|
{fallback_methods,
|
||||||
|
?HOCON(?ARRAY(binary()), #{
|
||||||
|
mapping => "oidcc.fallback_methods",
|
||||||
|
desc => ?DESC(fallback_methods),
|
||||||
|
default => [<<"RS256">>]
|
||||||
|
})},
|
||||||
|
{client_jwks,
|
||||||
|
%% TODO: add url JWKS
|
||||||
|
?HOCON(?UNION([none, ?R_REF(client_file_jwks)]), #{
|
||||||
|
desc => ?DESC(client_jwks),
|
||||||
|
default => none
|
||||||
|
})}
|
||||||
|
];
|
||||||
|
fields(client_file_jwks) ->
|
||||||
|
[
|
||||||
|
{type,
|
||||||
|
?HOCON(?ENUM([file]), #{
|
||||||
|
desc => ?DESC(client_file_jwks_type),
|
||||||
|
required => true
|
||||||
|
})},
|
||||||
|
{file,
|
||||||
|
?HOCON(binary(), #{
|
||||||
|
desc => ?DESC(client_file_jwks_file),
|
||||||
|
required => true
|
||||||
|
})}
|
||||||
|
];
|
||||||
|
fields(login) ->
|
||||||
|
[
|
||||||
|
emqx_dashboard_sso_schema:backend_schema([oidc])
|
||||||
|
].
|
||||||
|
|
||||||
|
desc(oidc) ->
|
||||||
|
"OIDC";
|
||||||
|
desc(client_file_jwks) ->
|
||||||
|
?DESC(client_file_jwks);
|
||||||
|
desc(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
create(#{name_var := NameVar} = Config) ->
|
||||||
|
case
|
||||||
|
emqx_dashboard_sso_oidc_session:start(
|
||||||
|
?PROVIDER_SVR_NAME,
|
||||||
|
Config
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{error, _} = Error ->
|
||||||
|
Error;
|
||||||
|
_ ->
|
||||||
|
%% Note: the oidcc maintains an ETS with the same name of the provider gen_server,
|
||||||
|
%% we should use this name in each API calls not the PID,
|
||||||
|
%% or it would backoff to sync calls to the gen_server
|
||||||
|
ClientJwks = init_client_jwks(Config),
|
||||||
|
{ok, #{
|
||||||
|
name => ?PROVIDER_SVR_NAME,
|
||||||
|
config => Config,
|
||||||
|
client_jwks => ClientJwks,
|
||||||
|
name_tokens => emqx_placeholder:preproc_tmpl(NameVar)
|
||||||
|
}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
update(Config, State) ->
|
||||||
|
destroy(State),
|
||||||
|
create(Config).
|
||||||
|
|
||||||
|
destroy(State) ->
|
||||||
|
emqx_dashboard_sso_oidc_session:stop(),
|
||||||
|
try_delete_jwks_file(State).
|
||||||
|
|
||||||
|
-dialyzer({nowarn_function, login/2}).
|
||||||
|
login(
|
||||||
|
_Req,
|
||||||
|
#{
|
||||||
|
client_jwks := ClientJwks,
|
||||||
|
config := #{
|
||||||
|
clientid := ClientId,
|
||||||
|
secret := Secret,
|
||||||
|
scopes := Scopes,
|
||||||
|
require_pkce := RequirePKCE,
|
||||||
|
preferred_auth_methods := AuthMethods
|
||||||
|
}
|
||||||
|
} = Cfg
|
||||||
|
) ->
|
||||||
|
Nonce = emqx_dashboard_sso_oidc_session:random_bin(),
|
||||||
|
Opts = maybe_require_pkce(RequirePKCE, #{
|
||||||
|
scopes => Scopes,
|
||||||
|
nonce => Nonce,
|
||||||
|
redirect_uri => emqx_dashboard_sso_oidc_api:make_callback_url(Cfg)
|
||||||
|
}),
|
||||||
|
|
||||||
|
Data = maps:with([nonce, require_pkce, pkce_verifier], Opts),
|
||||||
|
State = emqx_dashboard_sso_oidc_session:new(Data),
|
||||||
|
|
||||||
|
case
|
||||||
|
oidcc:create_redirect_url(
|
||||||
|
?PROVIDER_SVR_NAME,
|
||||||
|
ClientId,
|
||||||
|
emqx_secret:unwrap(Secret),
|
||||||
|
Opts#{
|
||||||
|
state => State,
|
||||||
|
client_jwks => ClientJwks,
|
||||||
|
preferred_auth_methods => AuthMethods
|
||||||
|
}
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{ok, [Base, Delimiter, Params]} ->
|
||||||
|
RedirectUri = <<Base/binary, Delimiter/binary, Params/binary>>,
|
||||||
|
Redirect = {302, ?RESPHEADERS#{<<"location">> => RedirectUri}, ?REDIRECT_BODY},
|
||||||
|
{redirect, Redirect};
|
||||||
|
{error, _Reason} = Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
convert_certs(
|
||||||
|
Dir,
|
||||||
|
#{
|
||||||
|
<<"client_jwks">> := #{
|
||||||
|
<<"type">> := file,
|
||||||
|
<<"file">> := Content
|
||||||
|
} = Jwks
|
||||||
|
} = Conf
|
||||||
|
) ->
|
||||||
|
case save_jwks_file(Dir, Content) of
|
||||||
|
{ok, Path} ->
|
||||||
|
Conf#{<<"client_jwks">> := Jwks#{<<"file">> := Path}};
|
||||||
|
{error, Reason} ->
|
||||||
|
?SLOG(error, #{msg => "failed_to_save_client_jwks", reason => Reason}),
|
||||||
|
throw("Failed to save client jwks")
|
||||||
|
end;
|
||||||
|
convert_certs(_Dir, Conf) ->
|
||||||
|
Conf.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
save_jwks_file(Dir, Content) ->
|
||||||
|
Path = filename:join([emqx_tls_lib:pem_dir(Dir), "client_jwks"]),
|
||||||
|
case filelib:ensure_dir(Path) of
|
||||||
|
ok ->
|
||||||
|
case file:write_file(Path, Content) of
|
||||||
|
ok ->
|
||||||
|
{ok, Path};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, #{failed_to_write_file => Reason, file_path => Path}}
|
||||||
|
end;
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, #{failed_to_create_dir_for => Path, reason => Reason}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
try_delete_jwks_file(#{config := #{client_jwks := #{type := file, file := File}}}) ->
|
||||||
|
_ = file:delete(File),
|
||||||
|
ok;
|
||||||
|
try_delete_jwks_file(_) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
maybe_require_pkce(false, Opts) ->
|
||||||
|
Opts;
|
||||||
|
maybe_require_pkce(true, Opts) ->
|
||||||
|
Opts#{
|
||||||
|
require_pkce => true,
|
||||||
|
pkce_verifier => emqx_dashboard_sso_oidc_session:random_bin(?PKCE_VERIFIER_LEN)
|
||||||
|
}.
|
||||||
|
|
||||||
|
init_client_jwks(#{client_jwks := #{type := file, file := File}}) ->
|
||||||
|
case jose_jwk:from_file(File) of
|
||||||
|
{error, _} ->
|
||||||
|
none;
|
||||||
|
Jwks ->
|
||||||
|
Jwks
|
||||||
|
end;
|
||||||
|
init_client_jwks(_) ->
|
||||||
|
none.
|
|
@ -0,0 +1,214 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_dashboard_sso_oidc_api).
|
||||||
|
|
||||||
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("emqx_dashboard/include/emqx_dashboard.hrl").
|
||||||
|
|
||||||
|
-import(hoconsc, [
|
||||||
|
mk/2,
|
||||||
|
array/1,
|
||||||
|
enum/1,
|
||||||
|
ref/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-import(emqx_dashboard_sso_api, [login_meta/3]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
api_spec/0,
|
||||||
|
paths/0,
|
||||||
|
schema/1,
|
||||||
|
namespace/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([code_callback/2, make_callback_url/1]).
|
||||||
|
|
||||||
|
-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD').
|
||||||
|
-define(BACKEND_NOT_FOUND, 'BACKEND_NOT_FOUND').
|
||||||
|
|
||||||
|
-define(RESPHEADERS, #{
|
||||||
|
<<"cache-control">> => <<"no-cache">>,
|
||||||
|
<<"pragma">> => <<"no-cache">>,
|
||||||
|
<<"content-type">> => <<"text/plain">>
|
||||||
|
}).
|
||||||
|
-define(REDIRECT_BODY, <<"Redirecting...">>).
|
||||||
|
|
||||||
|
-define(TAGS, <<"Dashboard Single Sign-On">>).
|
||||||
|
-define(BACKEND, oidc).
|
||||||
|
-define(BASE_PATH, "/api/v5").
|
||||||
|
-define(CALLBACK_PATH, "/sso/oidc/callback").
|
||||||
|
|
||||||
|
namespace() -> "dashboard_sso".
|
||||||
|
|
||||||
|
api_spec() ->
|
||||||
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false, translate_body => false}).
|
||||||
|
|
||||||
|
paths() ->
|
||||||
|
[
|
||||||
|
?CALLBACK_PATH
|
||||||
|
].
|
||||||
|
|
||||||
|
%% Handles Authorization Code callback from the OP.
|
||||||
|
schema("/sso/oidc/callback") ->
|
||||||
|
#{
|
||||||
|
'operationId' => code_callback,
|
||||||
|
get => #{
|
||||||
|
tags => [?TAGS],
|
||||||
|
desc => ?DESC(code_callback),
|
||||||
|
responses => #{
|
||||||
|
200 => emqx_dashboard_api:fields([token, version, license]),
|
||||||
|
401 => response_schema(401),
|
||||||
|
404 => response_schema(404)
|
||||||
|
},
|
||||||
|
security => []
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% API
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
code_callback(get, #{query_string := QS}) ->
|
||||||
|
case ensure_sso_state(QS) of
|
||||||
|
{ok, Target} ->
|
||||||
|
?SLOG(info, #{
|
||||||
|
msg => "dashboard_sso_login_successful"
|
||||||
|
}),
|
||||||
|
|
||||||
|
{302, ?RESPHEADERS#{<<"location">> => Target}, ?REDIRECT_BODY};
|
||||||
|
{error, invalid_backend} ->
|
||||||
|
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||||
|
{error, Reason} ->
|
||||||
|
?SLOG(info, #{
|
||||||
|
msg => "dashboard_sso_login_failed",
|
||||||
|
reason => emqx_utils:redact(Reason)
|
||||||
|
}),
|
||||||
|
{401, #{code => ?BAD_USERNAME_OR_PWD, message => reason_to_message(Reason)}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% internal
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
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)).
|
||||||
|
|
||||||
|
reason_to_message(Bin) when is_binary(Bin) ->
|
||||||
|
Bin;
|
||||||
|
reason_to_message(Term) ->
|
||||||
|
erlang:iolist_to_binary(io_lib:format("~p", [Term])).
|
||||||
|
|
||||||
|
ensure_sso_state(QS) ->
|
||||||
|
case emqx_dashboard_sso_manager:lookup_state(?BACKEND) of
|
||||||
|
undefined ->
|
||||||
|
{error, invalid_backend};
|
||||||
|
Cfg ->
|
||||||
|
ensure_oidc_state(QS, Cfg)
|
||||||
|
end.
|
||||||
|
|
||||||
|
ensure_oidc_state(#{<<"state">> := State} = QS, Cfg) ->
|
||||||
|
case emqx_dashboard_sso_oidc_session:lookup(State) of
|
||||||
|
{ok, Data} ->
|
||||||
|
emqx_dashboard_sso_oidc_session:delete(State),
|
||||||
|
retrieve_token(QS, Cfg, Data);
|
||||||
|
_ ->
|
||||||
|
{error, session_not_exists}
|
||||||
|
end.
|
||||||
|
|
||||||
|
retrieve_token(
|
||||||
|
#{<<"code">> := Code},
|
||||||
|
#{
|
||||||
|
name := Name,
|
||||||
|
client_jwks := ClientJwks,
|
||||||
|
config := #{
|
||||||
|
clientid := ClientId,
|
||||||
|
secret := Secret,
|
||||||
|
preferred_auth_methods := AuthMethods
|
||||||
|
}
|
||||||
|
} = Cfg,
|
||||||
|
Data
|
||||||
|
) ->
|
||||||
|
case
|
||||||
|
oidcc:retrieve_token(
|
||||||
|
Code,
|
||||||
|
Name,
|
||||||
|
ClientId,
|
||||||
|
emqx_secret:unwrap(Secret),
|
||||||
|
Data#{
|
||||||
|
redirect_uri => make_callback_url(Cfg),
|
||||||
|
client_jwks => ClientJwks,
|
||||||
|
preferred_auth_methods => AuthMethods
|
||||||
|
}
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{ok, Token} ->
|
||||||
|
retrieve_userinfo(Token, Cfg);
|
||||||
|
{error, _Reason} = Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
retrieve_userinfo(
|
||||||
|
Token,
|
||||||
|
#{
|
||||||
|
name := Name,
|
||||||
|
client_jwks := ClientJwks,
|
||||||
|
config := #{clientid := ClientId, secret := Secret},
|
||||||
|
name_tokens := NameTks
|
||||||
|
} = Cfg
|
||||||
|
) ->
|
||||||
|
case
|
||||||
|
oidcc:retrieve_userinfo(
|
||||||
|
Token,
|
||||||
|
Name,
|
||||||
|
ClientId,
|
||||||
|
emqx_secret:unwrap(Secret),
|
||||||
|
#{client_jwks => ClientJwks}
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{ok, UserInfo} ->
|
||||||
|
?SLOG(debug, #{
|
||||||
|
msg => "sso_oidc_login_user_info",
|
||||||
|
user_info => UserInfo
|
||||||
|
}),
|
||||||
|
Username = emqx_placeholder:proc_tmpl(NameTks, UserInfo),
|
||||||
|
ensure_user_exists(Cfg, Username);
|
||||||
|
{error, _Reason} = Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
-dialyzer({nowarn_function, ensure_user_exists/2}).
|
||||||
|
ensure_user_exists(_Cfg, <<>>) ->
|
||||||
|
{error, <<"Username can not be empty">>};
|
||||||
|
ensure_user_exists(_Cfg, <<"undefined">>) ->
|
||||||
|
{error, <<"Username can not be undefined">>};
|
||||||
|
ensure_user_exists(Cfg, Username) ->
|
||||||
|
case emqx_dashboard_admin:lookup_user(?BACKEND, Username) of
|
||||||
|
[User] ->
|
||||||
|
case emqx_dashboard_token:sign(User, <<>>) of
|
||||||
|
{ok, Role, Token} ->
|
||||||
|
{ok, login_redirect_target(Cfg, Username, Role, Token)};
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end;
|
||||||
|
[] ->
|
||||||
|
case emqx_dashboard_admin:add_sso_user(?BACKEND, Username, ?ROLE_VIEWER, <<>>) of
|
||||||
|
{ok, _} ->
|
||||||
|
ensure_user_exists(Cfg, Username);
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
make_callback_url(#{config := #{dashboard_addr := Addr}}) ->
|
||||||
|
list_to_binary(binary_to_list(Addr) ++ ?BASE_PATH ++ ?CALLBACK_PATH).
|
||||||
|
|
||||||
|
login_redirect_target(#{config := #{dashboard_addr := Addr}}, Username, Role, Token) ->
|
||||||
|
LoginMeta = emqx_dashboard_sso_api:login_meta(Username, Role, Token, oidc),
|
||||||
|
MetaBin = base64:encode(emqx_utils_json:encode(LoginMeta)),
|
||||||
|
<<Addr/binary, "/?login_meta=", MetaBin/binary>>.
|
|
@ -0,0 +1,157 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_dashboard_sso_oidc_session).
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("stdlib/include/ms_transform.hrl").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/1, start/2, stop/0]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([
|
||||||
|
init/1,
|
||||||
|
handle_call/3,
|
||||||
|
handle_cast/2,
|
||||||
|
handle_info/2,
|
||||||
|
terminate/2,
|
||||||
|
code_change/3,
|
||||||
|
format_status/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([new/1, delete/1, lookup/1, random_bin/0, random_bin/1]).
|
||||||
|
|
||||||
|
-define(TAB, ?MODULE).
|
||||||
|
|
||||||
|
-record(?TAB, {
|
||||||
|
state :: binary(),
|
||||||
|
created_at :: non_neg_integer(),
|
||||||
|
data :: map()
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(DEFAULT_RANDOM_LEN, 32).
|
||||||
|
-define(NOW, erlang:system_time(millisecond)).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% API
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
start_link(Cfg) ->
|
||||||
|
gen_server:start_link({local, ?MODULE}, ?MODULE, Cfg, []).
|
||||||
|
|
||||||
|
start(Name, #{issuer := Issuer, session_expiry := SessionExpiry0}) ->
|
||||||
|
case
|
||||||
|
emqx_dashboard_sso_sup:start_child(
|
||||||
|
oidcc_provider_configuration_worker,
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
issuer => Issuer,
|
||||||
|
name => {local, Name}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{error, _} = Error ->
|
||||||
|
Error;
|
||||||
|
_ ->
|
||||||
|
SessionExpiry = timer:seconds(SessionExpiry0),
|
||||||
|
emqx_dashboard_sso_sup:start_child(?MODULE, [SessionExpiry])
|
||||||
|
end.
|
||||||
|
|
||||||
|
stop() ->
|
||||||
|
_ = emqx_dashboard_sso_sup:stop_child(oidcc_provider_configuration_worker),
|
||||||
|
_ = emqx_dashboard_sso_sup:stop_child(?MODULE),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
new(Data) ->
|
||||||
|
State = new_state(),
|
||||||
|
ets:insert(
|
||||||
|
?TAB,
|
||||||
|
#?TAB{
|
||||||
|
state = State,
|
||||||
|
created_at = ?NOW,
|
||||||
|
data = Data
|
||||||
|
}
|
||||||
|
),
|
||||||
|
State.
|
||||||
|
|
||||||
|
delete(State) ->
|
||||||
|
ets:delete(?TAB, State).
|
||||||
|
|
||||||
|
lookup(State) ->
|
||||||
|
case ets:lookup(?TAB, State) of
|
||||||
|
[#?TAB{data = Data}] ->
|
||||||
|
{ok, Data};
|
||||||
|
_ ->
|
||||||
|
undefined
|
||||||
|
end.
|
||||||
|
|
||||||
|
random_bin() ->
|
||||||
|
random_bin(?DEFAULT_RANDOM_LEN).
|
||||||
|
|
||||||
|
random_bin(Len) ->
|
||||||
|
emqx_utils_conv:bin(emqx_utils:gen_id(Len)).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% gen_server callbacks
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
init(SessionExpiry) ->
|
||||||
|
process_flag(trap_exit, true),
|
||||||
|
emqx_utils_ets:new(
|
||||||
|
?TAB,
|
||||||
|
[
|
||||||
|
ordered_set,
|
||||||
|
public,
|
||||||
|
named_table,
|
||||||
|
{keypos, #?TAB.state},
|
||||||
|
{read_concurrency, true}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
State = #{session_expiry => SessionExpiry},
|
||||||
|
tick_session_expiry(State),
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
handle_call(_Request, _From, State) ->
|
||||||
|
Reply = ok,
|
||||||
|
{reply, Reply, State}.
|
||||||
|
|
||||||
|
handle_cast(_Request, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info(tick_session_expiry, #{session_expiry := SessionExpiry} = State) ->
|
||||||
|
Now = ?NOW,
|
||||||
|
Spec = ets:fun2ms(fun(#?TAB{created_at = CreatedAt}) ->
|
||||||
|
Now - CreatedAt >= SessionExpiry
|
||||||
|
end),
|
||||||
|
_ = ets:select_delete(?TAB, Spec),
|
||||||
|
tick_session_expiry(State),
|
||||||
|
{noreply, State};
|
||||||
|
handle_info(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
format_status(_Opt, Status) ->
|
||||||
|
Status.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
new_state() ->
|
||||||
|
State = random_bin(),
|
||||||
|
case ets:lookup(?TAB, State) of
|
||||||
|
[] ->
|
||||||
|
State;
|
||||||
|
_ ->
|
||||||
|
new_state()
|
||||||
|
end.
|
||||||
|
|
||||||
|
tick_session_expiry(#{session_expiry := SessionExpiry}) ->
|
||||||
|
erlang:send_after(SessionExpiry, self(), tick_session_expiry).
|
|
@ -273,7 +273,7 @@ is_msie(Headers) ->
|
||||||
not (binary:match(UA, <<"MSIE">>) =:= nomatch).
|
not (binary:match(UA, <<"MSIE">>) =:= nomatch).
|
||||||
|
|
||||||
login_redirect_target(DashboardAddr, Username, Role, Token) ->
|
login_redirect_target(DashboardAddr, Username, Role, Token) ->
|
||||||
LoginMeta = emqx_dashboard_sso_api:login_meta(Username, Role, Token),
|
LoginMeta = emqx_dashboard_sso_api:login_meta(Username, Role, Token, saml),
|
||||||
<<DashboardAddr/binary, "/?login_meta=", (base64_login_meta(LoginMeta))/binary>>.
|
<<DashboardAddr/binary, "/?login_meta=", (base64_login_meta(LoginMeta))/binary>>.
|
||||||
|
|
||||||
base64_login_meta(LoginMeta) ->
|
base64_login_meta(LoginMeta) ->
|
||||||
|
|
|
@ -6,17 +6,26 @@
|
||||||
|
|
||||||
-behaviour(supervisor).
|
-behaviour(supervisor).
|
||||||
|
|
||||||
-export([start_link/0]).
|
-export([start_link/0, start_child/2, stop_child/1]).
|
||||||
|
|
||||||
-export([init/1]).
|
-export([init/1]).
|
||||||
|
|
||||||
-define(CHILD(I, ShutDown), {I, {I, start_link, []}, permanent, ShutDown, worker, [I]}).
|
-define(CHILD(I, Args, Restart), {I, {I, start_link, Args}, Restart, 5000, worker, [I]}).
|
||||||
|
-define(CHILD(I), ?CHILD(I, [], permanent)).
|
||||||
|
|
||||||
start_link() ->
|
start_link() ->
|
||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
|
start_child(Mod, Args) ->
|
||||||
|
supervisor:start_child(?MODULE, ?CHILD(Mod, Args, transient)).
|
||||||
|
|
||||||
|
stop_child(Mod) ->
|
||||||
|
_ = supervisor:terminate_child(?MODULE, Mod),
|
||||||
|
_ = supervisor:delete_child(?MODULE, Mod),
|
||||||
|
ok.
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
{ok,
|
{ok,
|
||||||
{{one_for_one, 5, 100}, [
|
{{one_for_one, 5, 100}, [
|
||||||
?CHILD(emqx_dashboard_sso_manager, 5000)
|
?CHILD(emqx_dashboard_sso_manager)
|
||||||
]}}.
|
]}}.
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Check the `PasswordFlag` of the MQTT v3.1.1 CONNECT packet in strict mode to comply with the protocol.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> To ensure BUG-TO-BUG compatibility, this check is performed only in strict mode.
|
4
mix.exs
4
mix.exs
|
@ -68,14 +68,14 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
{:rulesql, github: "emqx/rulesql", tag: "0.2.1"},
|
{:rulesql, github: "emqx/rulesql", tag: "0.2.1"},
|
||||||
{:observer_cli, "1.7.1"},
|
{:observer_cli, "1.7.1"},
|
||||||
{:system_monitor, github: "ieQu1/system_monitor", tag: "3.0.5"},
|
{:system_monitor, github: "ieQu1/system_monitor", tag: "3.0.5"},
|
||||||
{:telemetry, "1.1.0"},
|
{:telemetry, "1.1.0", override: true},
|
||||||
# in conflict by emqtt and hocon
|
# in conflict by emqtt and hocon
|
||||||
{:getopt, "1.0.2", override: true},
|
{:getopt, "1.0.2", override: true},
|
||||||
{:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "1.0.10", override: true},
|
{:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "1.0.10", override: true},
|
||||||
{:hocon, github: "emqx/hocon", tag: "0.42.2", override: true},
|
{:hocon, github: "emqx/hocon", tag: "0.42.2", override: true},
|
||||||
{:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.5.3", override: true},
|
{:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.5.3", override: true},
|
||||||
{:esasl, github: "emqx/esasl", tag: "0.2.1"},
|
{:esasl, github: "emqx/esasl", tag: "0.2.1"},
|
||||||
{:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"},
|
{:jose, github: "potatosalad/erlang-jose", tag: "1.11.2", override: true},
|
||||||
# in conflict by ehttpc and emqtt
|
# in conflict by ehttpc and emqtt
|
||||||
{:gun, github: "emqx/gun", tag: "1.3.11", override: true},
|
{:gun, github: "emqx/gun", tag: "1.3.11", override: true},
|
||||||
# in conflict by emqx_connector and system_monitor
|
# in conflict by emqx_connector and system_monitor
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
emqx_dashboard_sso_oidc {
|
||||||
|
|
||||||
|
issuer.desc:
|
||||||
|
"""The URL of the OIDC issuer."""
|
||||||
|
|
||||||
|
clientid.desc:
|
||||||
|
"""The clientId for this backend."""
|
||||||
|
|
||||||
|
secret.desc:
|
||||||
|
"""The client secret."""
|
||||||
|
|
||||||
|
scopes.desc:
|
||||||
|
"""The scopes, its default value is `["openid"]`."""
|
||||||
|
|
||||||
|
name_var.desc:
|
||||||
|
"""A template to map OIDC user information to a Dashboard name, its default value is `${sub}`."""
|
||||||
|
|
||||||
|
dashboard_addr.desc:
|
||||||
|
"""The address of the EMQX Dashboard."""
|
||||||
|
|
||||||
|
session_expiry.desc:
|
||||||
|
"""The valid time span for an OIDC `state`, the default is `30s`, if the code response returned by the authorization server exceeds this time span, it will be treated as invalid."""
|
||||||
|
|
||||||
|
require_pkce.desc:
|
||||||
|
"""Whether to require PKCE when getting the token."""
|
||||||
|
|
||||||
|
client_jwks.desc:
|
||||||
|
"""Set JWK or JWKS here to enable the `private_key_jwt` authorization or the `DPoP` extension."""
|
||||||
|
|
||||||
|
client_file_jwks_type.desc:
|
||||||
|
"""The JWKS source type."""
|
||||||
|
|
||||||
|
client_file_jwks.desc:
|
||||||
|
"""Set JWKS from file."""
|
||||||
|
|
||||||
|
client_file_jwks_file.desc:
|
||||||
|
"""The content of the JWKS."""
|
||||||
|
|
||||||
|
preferred_auth_methods.desc:
|
||||||
|
"""Set the valid authentication methods and their priority."""
|
||||||
|
|
||||||
|
provider.desc:
|
||||||
|
"""The OIDC provider."""
|
||||||
|
|
||||||
|
fallback_methods.desc:
|
||||||
|
"""Some providers do not provide all the method items in the provider configuration, set this value as a fallback for those items."""
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
emqx_dashboard_sso_oidc_api {
|
||||||
|
|
||||||
|
code_callback.desc:
|
||||||
|
"""The callback path for the OIDC authorization server.."""
|
||||||
|
|
||||||
|
}
|
|
@ -311,3 +311,4 @@ doc_as_upsert
|
||||||
upsert
|
upsert
|
||||||
aliyun
|
aliyun
|
||||||
OID
|
OID
|
||||||
|
PKCE
|
||||||
|
|
Loading…
Reference in New Issue