feat(oidc): implement JWKS, private_key_jwt, DPoP

This commit is contained in:
firest 2024-06-21 20:25:25 +08:00
parent 9c0df3c0a8
commit ddb197951e
4 changed files with 102 additions and 10 deletions

View File

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

View File

@ -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} ->

View File

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

View File

@ -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."""
}