feat(oidc): implement JWKS, private_key_jwt, DPoP
This commit is contained in:
parent
9c0df3c0a8
commit
ddb197951e
|
@ -90,6 +90,25 @@ fields(oidc) ->
|
|||
?HOCON(boolean(), #{
|
||||
desc => ?DESC(require_pkce),
|
||||
default => false
|
||||
})},
|
||||
{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) ->
|
||||
|
@ -119,9 +138,11 @@ create(#{name_var := NameVar} = Config) ->
|
|||
%% 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.
|
||||
|
@ -130,12 +151,14 @@ update(Config, State) ->
|
|||
destroy(State),
|
||||
create(Config).
|
||||
|
||||
destroy(_) ->
|
||||
emqx_dashboard_sso_oidc_session:stop().
|
||||
destroy(State) ->
|
||||
emqx_dashboard_sso_oidc_session:stop(),
|
||||
try_delete_jwks_file(State).
|
||||
|
||||
login(
|
||||
_Req,
|
||||
#{
|
||||
client_jwks := ClientJwks,
|
||||
config := #{
|
||||
clientid := ClientId,
|
||||
secret := Secret,
|
||||
|
@ -159,7 +182,7 @@ login(
|
|||
?PROVIDER_SVR_NAME,
|
||||
ClientId,
|
||||
Secret,
|
||||
Opts#{state => State}
|
||||
Opts#{state => State, client_jwks => ClientJwks}
|
||||
)
|
||||
of
|
||||
{ok, [Base, Delimiter, Params]} ->
|
||||
|
@ -170,9 +193,49 @@ login(
|
|||
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) ->
|
||||
|
@ -180,3 +243,13 @@ maybe_require_pkce(true, 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.
|
||||
|
|
|
@ -114,7 +114,11 @@ ensure_oidc_state(#{<<"state">> := State} = QS, Cfg) ->
|
|||
|
||||
retrieve_token(
|
||||
#{<<"code">> := Code},
|
||||
#{name := Name, config := #{clientid := ClientId, secret := Secret}} = Cfg,
|
||||
#{
|
||||
name := Name,
|
||||
client_jwks := ClientJwks,
|
||||
config := #{clientid := ClientId, secret := Secret}
|
||||
} = Cfg,
|
||||
Data
|
||||
) ->
|
||||
case
|
||||
|
@ -123,7 +127,10 @@ retrieve_token(
|
|||
Name,
|
||||
ClientId,
|
||||
Secret,
|
||||
Data#{redirect_uri => make_callback_url(Cfg)}
|
||||
Data#{
|
||||
redirect_uri => make_callback_url(Cfg),
|
||||
client_jwks => ClientJwks
|
||||
}
|
||||
)
|
||||
of
|
||||
{ok, Token} ->
|
||||
|
@ -134,6 +141,7 @@ retrieve_token(
|
|||
|
||||
retrieve_userinfo(Token, #{
|
||||
name := Name,
|
||||
client_jwks := ClientJwks,
|
||||
config := #{clientid := ClientId, secret := Secret},
|
||||
name_tokens := NameTks
|
||||
}) ->
|
||||
|
@ -143,7 +151,7 @@ retrieve_userinfo(Token, #{
|
|||
Name,
|
||||
ClientId,
|
||||
Secret,
|
||||
#{}
|
||||
#{client_jwks => ClientJwks}
|
||||
)
|
||||
of
|
||||
{ok, UserInfo} ->
|
||||
|
|
|
@ -10,17 +10,19 @@
|
|||
|
||||
-export([init/1]).
|
||||
|
||||
-define(CHILD(I, Args), {I, {I, start_link, Args}, permanent, 5000, worker, [I]}).
|
||||
-define(CHILD(I), ?CHILD(I, [])).
|
||||
-define(CHILD(I, Args, Restart), {I, {I, start_link, Args}, Restart, 5000, worker, [I]}).
|
||||
-define(CHILD(I), ?CHILD(I, [], permanent)).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
start_child(Mod, Args) ->
|
||||
supervisor:start_child(?MODULE, ?CHILD(Mod, Args)).
|
||||
supervisor:start_child(?MODULE, ?CHILD(Mod, Args, transient)).
|
||||
|
||||
stop_child(Mod) ->
|
||||
supervisor:terminate_child(?MODULE, Mod).
|
||||
_ = supervisor:terminate_child(?MODULE, Mod),
|
||||
_ = supervisor:delete_child(?MODULE, Mod),
|
||||
ok.
|
||||
|
||||
init([]) ->
|
||||
{ok,
|
||||
|
|
|
@ -24,4 +24,13 @@ session_expiry.desc:
|
|||
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_file.desc:
|
||||
"""The content of the JWKS."""
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue