feat(oidc): implement JWKS, private_key_jwt, DPoP
This commit is contained in:
parent
9c0df3c0a8
commit
ddb197951e
|
@ -90,8 +90,27 @@ fields(oidc) ->
|
||||||
?HOCON(boolean(), #{
|
?HOCON(boolean(), #{
|
||||||
desc => ?DESC(require_pkce),
|
desc => ?DESC(require_pkce),
|
||||||
default => false
|
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) ->
|
fields(login) ->
|
||||||
[
|
[
|
||||||
emqx_dashboard_sso_schema:backend_schema([oidc])
|
emqx_dashboard_sso_schema:backend_schema([oidc])
|
||||||
|
@ -119,9 +138,11 @@ create(#{name_var := NameVar} = Config) ->
|
||||||
%% Note: the oidcc maintains an ETS with the same name of the provider gen_server,
|
%% 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,
|
%% we should use this name in each API calls not the PID,
|
||||||
%% or it would backoff to sync calls to the gen_server
|
%% or it would backoff to sync calls to the gen_server
|
||||||
|
ClientJwks = init_client_jwks(Config),
|
||||||
{ok, #{
|
{ok, #{
|
||||||
name => ?PROVIDER_SVR_NAME,
|
name => ?PROVIDER_SVR_NAME,
|
||||||
config => Config,
|
config => Config,
|
||||||
|
client_jwks => ClientJwks,
|
||||||
name_tokens => emqx_placeholder:preproc_tmpl(NameVar)
|
name_tokens => emqx_placeholder:preproc_tmpl(NameVar)
|
||||||
}}
|
}}
|
||||||
end.
|
end.
|
||||||
|
@ -130,12 +151,14 @@ update(Config, State) ->
|
||||||
destroy(State),
|
destroy(State),
|
||||||
create(Config).
|
create(Config).
|
||||||
|
|
||||||
destroy(_) ->
|
destroy(State) ->
|
||||||
emqx_dashboard_sso_oidc_session:stop().
|
emqx_dashboard_sso_oidc_session:stop(),
|
||||||
|
try_delete_jwks_file(State).
|
||||||
|
|
||||||
login(
|
login(
|
||||||
_Req,
|
_Req,
|
||||||
#{
|
#{
|
||||||
|
client_jwks := ClientJwks,
|
||||||
config := #{
|
config := #{
|
||||||
clientid := ClientId,
|
clientid := ClientId,
|
||||||
secret := Secret,
|
secret := Secret,
|
||||||
|
@ -159,7 +182,7 @@ login(
|
||||||
?PROVIDER_SVR_NAME,
|
?PROVIDER_SVR_NAME,
|
||||||
ClientId,
|
ClientId,
|
||||||
Secret,
|
Secret,
|
||||||
Opts#{state => State}
|
Opts#{state => State, client_jwks => ClientJwks}
|
||||||
)
|
)
|
||||||
of
|
of
|
||||||
{ok, [Base, Delimiter, Params]} ->
|
{ok, [Base, Delimiter, Params]} ->
|
||||||
|
@ -170,9 +193,49 @@ login(
|
||||||
Error
|
Error
|
||||||
end.
|
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) ->
|
convert_certs(_Dir, Conf) ->
|
||||||
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) ->
|
maybe_require_pkce(false, Opts) ->
|
||||||
Opts;
|
Opts;
|
||||||
maybe_require_pkce(true, Opts) ->
|
maybe_require_pkce(true, Opts) ->
|
||||||
|
@ -180,3 +243,13 @@ maybe_require_pkce(true, Opts) ->
|
||||||
require_pkce => true,
|
require_pkce => true,
|
||||||
pkce_verifier => emqx_dashboard_sso_oidc_session:random_bin(?PKCE_VERIFIER_LEN)
|
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(
|
retrieve_token(
|
||||||
#{<<"code">> := Code},
|
#{<<"code">> := Code},
|
||||||
#{name := Name, config := #{clientid := ClientId, secret := Secret}} = Cfg,
|
#{
|
||||||
|
name := Name,
|
||||||
|
client_jwks := ClientJwks,
|
||||||
|
config := #{clientid := ClientId, secret := Secret}
|
||||||
|
} = Cfg,
|
||||||
Data
|
Data
|
||||||
) ->
|
) ->
|
||||||
case
|
case
|
||||||
|
@ -123,7 +127,10 @@ retrieve_token(
|
||||||
Name,
|
Name,
|
||||||
ClientId,
|
ClientId,
|
||||||
Secret,
|
Secret,
|
||||||
Data#{redirect_uri => make_callback_url(Cfg)}
|
Data#{
|
||||||
|
redirect_uri => make_callback_url(Cfg),
|
||||||
|
client_jwks => ClientJwks
|
||||||
|
}
|
||||||
)
|
)
|
||||||
of
|
of
|
||||||
{ok, Token} ->
|
{ok, Token} ->
|
||||||
|
@ -134,6 +141,7 @@ retrieve_token(
|
||||||
|
|
||||||
retrieve_userinfo(Token, #{
|
retrieve_userinfo(Token, #{
|
||||||
name := Name,
|
name := Name,
|
||||||
|
client_jwks := ClientJwks,
|
||||||
config := #{clientid := ClientId, secret := Secret},
|
config := #{clientid := ClientId, secret := Secret},
|
||||||
name_tokens := NameTks
|
name_tokens := NameTks
|
||||||
}) ->
|
}) ->
|
||||||
|
@ -143,7 +151,7 @@ retrieve_userinfo(Token, #{
|
||||||
Name,
|
Name,
|
||||||
ClientId,
|
ClientId,
|
||||||
Secret,
|
Secret,
|
||||||
#{}
|
#{client_jwks => ClientJwks}
|
||||||
)
|
)
|
||||||
of
|
of
|
||||||
{ok, UserInfo} ->
|
{ok, UserInfo} ->
|
||||||
|
|
|
@ -10,17 +10,19 @@
|
||||||
|
|
||||||
-export([init/1]).
|
-export([init/1]).
|
||||||
|
|
||||||
-define(CHILD(I, Args), {I, {I, start_link, Args}, permanent, 5000, worker, [I]}).
|
-define(CHILD(I, Args, Restart), {I, {I, start_link, Args}, Restart, 5000, worker, [I]}).
|
||||||
-define(CHILD(I), ?CHILD(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) ->
|
start_child(Mod, Args) ->
|
||||||
supervisor:start_child(?MODULE, ?CHILD(Mod, Args)).
|
supervisor:start_child(?MODULE, ?CHILD(Mod, Args, transient)).
|
||||||
|
|
||||||
stop_child(Mod) ->
|
stop_child(Mod) ->
|
||||||
supervisor:terminate_child(?MODULE, Mod).
|
_ = supervisor:terminate_child(?MODULE, Mod),
|
||||||
|
_ = supervisor:delete_child(?MODULE, Mod),
|
||||||
|
ok.
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
{ok,
|
{ok,
|
||||||
|
|
|
@ -24,4 +24,13 @@ session_expiry.desc:
|
||||||
require_pkce.desc:
|
require_pkce.desc:
|
||||||
"""Whether to require PKCE when getting the token."""
|
"""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