feat(oidc): implement session management
This commit is contained in:
parent
512b4b9cbb
commit
5e2693c9b4
|
@ -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
|
||||||
|
@ -154,8 +155,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 +167,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.
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
convert_certs/2
|
convert_certs/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-define(PROVIDER_SVR_NAME, sso_oidc_provider).
|
-define(PROVIDER_SVR_NAME, ?MODULE).
|
||||||
-define(RESPHEADERS, #{
|
-define(RESPHEADERS, #{
|
||||||
<<"cache-control">> => <<"no-cache">>,
|
<<"cache-control">> => <<"no-cache">>,
|
||||||
<<"pragma">> => <<"no-cache">>,
|
<<"pragma">> => <<"no-cache">>,
|
||||||
|
@ -79,6 +79,11 @@ fields(oidc) ->
|
||||||
?HOCON(binary(), #{
|
?HOCON(binary(), #{
|
||||||
desc => ?DESC(dashboard_addr),
|
desc => ?DESC(dashboard_addr),
|
||||||
default => <<"http://127.0.0.1:18083">>
|
default => <<"http://127.0.0.1:18083">>
|
||||||
|
})},
|
||||||
|
{session_expiry,
|
||||||
|
?HOCON(emqx_schema:timeout_duration_ms(), #{
|
||||||
|
desc => ?DESC(session_expiry),
|
||||||
|
default => <<"30s">>
|
||||||
})}
|
})}
|
||||||
];
|
];
|
||||||
fields(login) ->
|
fields(login) ->
|
||||||
|
@ -95,30 +100,32 @@ desc(_) ->
|
||||||
%% APIs
|
%% APIs
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
create(#{issuer := Issuer, name_var := NameVar} = Config) ->
|
create(#{name_var := NameVar} = Config) ->
|
||||||
case
|
case
|
||||||
oidcc_provider_configuration_worker:start_link(#{
|
emqx_dashboard_sso_oidc_session:start(
|
||||||
issuer => Issuer,
|
?PROVIDER_SVR_NAME,
|
||||||
name => {local, ?PROVIDER_SVR_NAME}
|
Config
|
||||||
})
|
)
|
||||||
of
|
of
|
||||||
{ok, Pid} ->
|
{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
|
||||||
{ok, #{
|
{ok, #{
|
||||||
pid => Pid,
|
name => ?PROVIDER_SVR_NAME,
|
||||||
config => Config,
|
config => Config,
|
||||||
name_tokens => emqx_placeholder:preproc_tmpl(NameVar)
|
name_tokens => emqx_placeholder:preproc_tmpl(NameVar)
|
||||||
}};
|
}}
|
||||||
{error, _} = Error ->
|
|
||||||
Error
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
update(Config, State) ->
|
update(Config, State) ->
|
||||||
destroy(State),
|
destroy(State),
|
||||||
create(Config).
|
create(Config).
|
||||||
|
|
||||||
destroy(#{pid := Pid}) ->
|
destroy(_) ->
|
||||||
_ = catch gen_server:stop(Pid),
|
emqx_dashboard_sso_oidc_session:stop().
|
||||||
ok.
|
|
||||||
|
|
||||||
login(
|
login(
|
||||||
_Req,
|
_Req,
|
||||||
|
@ -128,8 +135,12 @@ login(
|
||||||
secret := Secret,
|
secret := Secret,
|
||||||
scopes := Scopes
|
scopes := Scopes
|
||||||
}
|
}
|
||||||
} = State
|
} = Cfg
|
||||||
) ->
|
) ->
|
||||||
|
Nonce = emqx_dashboard_sso_oidc_session:random_bin(),
|
||||||
|
Data = #{nonce => Nonce},
|
||||||
|
|
||||||
|
State = emqx_dashboard_sso_oidc_session:new(Data),
|
||||||
case
|
case
|
||||||
oidcc:create_redirect_url(
|
oidcc:create_redirect_url(
|
||||||
?PROVIDER_SVR_NAME,
|
?PROVIDER_SVR_NAME,
|
||||||
|
@ -137,9 +148,9 @@ login(
|
||||||
Secret,
|
Secret,
|
||||||
#{
|
#{
|
||||||
scopes => Scopes,
|
scopes => Scopes,
|
||||||
state => random_bin(),
|
state => State,
|
||||||
nonce => random_bin(),
|
nonce => Nonce,
|
||||||
redirect_uri => emqx_dashboard_sso_oidc_api:make_callback_url(State)
|
redirect_uri => emqx_dashboard_sso_oidc_api:make_callback_url(Cfg)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
of
|
of
|
||||||
|
@ -153,6 +164,3 @@ login(
|
||||||
|
|
||||||
convert_certs(_Dir, Conf) ->
|
convert_certs(_Dir, Conf) ->
|
||||||
Conf.
|
Conf.
|
||||||
|
|
||||||
random_bin() ->
|
|
||||||
emqx_utils_conv:bin(emqx_utils:gen_id(16)).
|
|
||||||
|
|
|
@ -64,66 +64,26 @@ schema("/sso/oidc/callback") ->
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% API
|
%% API
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
code_callback(get, #{query_string := #{<<"code">> := Code}}) ->
|
code_callback(get, #{query_string := QS}) ->
|
||||||
case emqx_dashboard_sso_manager:lookup_state(?BACKEND) of
|
case ensure_sso_state(QS) of
|
||||||
#{pid := Pid, config := #{clientid := ClientId, secret := Secret}} = State ->
|
{ok, Username, Role, DashboardToken} ->
|
||||||
case
|
?SLOG(info, #{
|
||||||
oidcc:retrieve_token(
|
msg => "dashboard_sso_login_successful"
|
||||||
Code,
|
}),
|
||||||
Pid,
|
{200, login_meta(Username, Role, DashboardToken)};
|
||||||
ClientId,
|
{error, invalid_backend} ->
|
||||||
Secret,
|
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||||
#{redirect_uri => make_callback_url(State)}
|
{error, Reason} ->
|
||||||
)
|
?SLOG(info, #{
|
||||||
of
|
msg => "dashboard_sso_login_failed",
|
||||||
{ok, Token} ->
|
reason => emqx_utils:redact(Reason)
|
||||||
retrieve_userinfo(Token, State);
|
}),
|
||||||
{error, Reason} ->
|
{401, #{code => ?BAD_USERNAME_OR_PWD, message => reason_to_message(Reason)}}
|
||||||
{401, #{code => ?BAD_USERNAME_OR_PWD, message => reason_to_message(Reason)}}
|
|
||||||
end;
|
|
||||||
_ ->
|
|
||||||
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% internal
|
%% internal
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
retrieve_userinfo(Token, #{
|
|
||||||
pid := Pid,
|
|
||||||
config := #{clientid := ClientId, secret := Secret},
|
|
||||||
name_tokens := NameTks
|
|
||||||
}) ->
|
|
||||||
case
|
|
||||||
oidcc:retrieve_userinfo(
|
|
||||||
Token,
|
|
||||||
Pid,
|
|
||||||
ClientId,
|
|
||||||
Secret,
|
|
||||||
#{}
|
|
||||||
)
|
|
||||||
of
|
|
||||||
{ok, UserInfo} ->
|
|
||||||
?SLOG(debug, #{
|
|
||||||
msg => "sso_oidc_login_user_info",
|
|
||||||
user_info => UserInfo
|
|
||||||
}),
|
|
||||||
Username = emqx_placeholder:proc_tmpl(NameTks, UserInfo),
|
|
||||||
case ensure_user_exists(Username) of
|
|
||||||
{ok, Role, DashboardToken} ->
|
|
||||||
?SLOG(info, #{
|
|
||||||
msg => "dashboard_sso_login_successful"
|
|
||||||
}),
|
|
||||||
{200, login_meta(Username, Role, DashboardToken)};
|
|
||||||
{error, Reason} ->
|
|
||||||
?SLOG(info, #{
|
|
||||||
msg => "dashboard_sso_login_failed",
|
|
||||||
reason => emqx_utils:redact(Reason)
|
|
||||||
}),
|
|
||||||
{401, #{code => ?BAD_USERNAME_OR_PWD, message => <<"Auth failed">>}}
|
|
||||||
end;
|
|
||||||
{error, Reason} ->
|
|
||||||
{401, #{code => ?BAD_USERNAME_OR_PWD, message => reason_to_message(Reason)}}
|
|
||||||
end.
|
|
||||||
|
|
||||||
response_schema(401) ->
|
response_schema(401) ->
|
||||||
emqx_dashboard_swagger:error_codes([?BAD_USERNAME_OR_PWD], ?DESC(login_failed401));
|
emqx_dashboard_swagger:error_codes([?BAD_USERNAME_OR_PWD], ?DESC(login_failed401));
|
||||||
|
@ -135,6 +95,68 @@ reason_to_message(Bin) when is_binary(Bin) ->
|
||||||
reason_to_message(Term) ->
|
reason_to_message(Term) ->
|
||||||
erlang:iolist_to_binary(io_lib:format("~p", [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, config := #{clientid := ClientId, secret := Secret}} = Cfg,
|
||||||
|
#{nonce := Nonce} = _Data
|
||||||
|
) ->
|
||||||
|
case
|
||||||
|
oidcc:retrieve_token(
|
||||||
|
Code,
|
||||||
|
Name,
|
||||||
|
ClientId,
|
||||||
|
Secret,
|
||||||
|
#{redirect_uri => make_callback_url(Cfg), nonce => Nonce}
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{ok, Token} ->
|
||||||
|
retrieve_userinfo(Token, Cfg);
|
||||||
|
{error, _Reason} = Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
retrieve_userinfo(Token, #{
|
||||||
|
name := Name,
|
||||||
|
config := #{clientid := ClientId, secret := Secret},
|
||||||
|
name_tokens := NameTks
|
||||||
|
}) ->
|
||||||
|
case
|
||||||
|
oidcc:retrieve_userinfo(
|
||||||
|
Token,
|
||||||
|
Name,
|
||||||
|
ClientId,
|
||||||
|
Secret,
|
||||||
|
#{}
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{ok, UserInfo} ->
|
||||||
|
?SLOG(debug, #{
|
||||||
|
msg => "sso_oidc_login_user_info",
|
||||||
|
user_info => UserInfo
|
||||||
|
}),
|
||||||
|
Username = emqx_placeholder:proc_tmpl(NameTks, UserInfo),
|
||||||
|
ensure_user_exists(Username);
|
||||||
|
{error, _Reason} = Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
ensure_user_exists(<<>>) ->
|
ensure_user_exists(<<>>) ->
|
||||||
{error, <<"Username can not be empty">>};
|
{error, <<"Username can not be empty">>};
|
||||||
ensure_user_exists(<<"undefined">>) ->
|
ensure_user_exists(<<"undefined">>) ->
|
||||||
|
@ -142,7 +164,12 @@ ensure_user_exists(<<"undefined">>) ->
|
||||||
ensure_user_exists(Username) ->
|
ensure_user_exists(Username) ->
|
||||||
case emqx_dashboard_admin:lookup_user(?BACKEND, Username) of
|
case emqx_dashboard_admin:lookup_user(?BACKEND, Username) of
|
||||||
[User] ->
|
[User] ->
|
||||||
emqx_dashboard_token:sign(User, <<>>);
|
case emqx_dashboard_token:sign(User, <<>>) of
|
||||||
|
{ok, Role, Token} ->
|
||||||
|
{ok, Username, Role, Token};
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end;
|
||||||
[] ->
|
[] ->
|
||||||
case emqx_dashboard_admin:add_sso_user(?BACKEND, Username, ?ROLE_VIEWER, <<>>) of
|
case emqx_dashboard_admin:add_sso_user(?BACKEND, Username, ?ROLE_VIEWER, <<>>) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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 := SessionExpiry}) ->
|
||||||
|
case
|
||||||
|
emqx_dashboard_sso_sup:start_child(
|
||||||
|
oidcc_provider_configuration_worker,
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
issuer => Issuer,
|
||||||
|
name => {local, Name}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{error, _} = Error ->
|
||||||
|
Error;
|
||||||
|
_ ->
|
||||||
|
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).
|
|
@ -6,17 +6,24 @@
|
||||||
|
|
||||||
-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), {I, {I, start_link, Args}, permanent, 5000, worker, [I]}).
|
||||||
|
-define(CHILD(I), ?CHILD(I, [])).
|
||||||
|
|
||||||
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)).
|
||||||
|
|
||||||
|
stop_child(Mod) ->
|
||||||
|
supervisor:terminate_child(?MODULE, Mod).
|
||||||
|
|
||||||
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)
|
||||||
]}}.
|
]}}.
|
||||||
|
|
|
@ -18,4 +18,7 @@ name_var.desc:
|
||||||
dashboard_addr.desc:
|
dashboard_addr.desc:
|
||||||
"""The address of the EMQX Dashboard."""
|
"""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."""
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue