feat(swarm-authelia): let an oidc client say it is a machine client
`renderClient` could only emit the authorization-code shape, so a
daemon client was expressed as an interactive one with an empty
redirect list. Authelia permits only the grants a client names, and an
omitted `grant_types` means authorization-code alone — so that shape
cannot obtain a token at all.
Measured against authelia 4.39.20, rendering exactly what this module
produced for `swarm-nats`:
client_secret_basic → unauthorized_client: The OAuth 2.0 Client is
not allowed to use authorization grant
'client_credentials'
introspection → {"active":false} (works)
Introspection is all the queue's responder needs today, which is why
nothing was visibly broken while the comment in `swarm-nats.nix`
described a grant that was never configured.
Adds `kind = "interactive" | "machine"` rather than inferring from an
empty `redirectUris`, because the two differ in what authelia permits
and not merely in what is populated. `openid` is dropped from a machine
client's scopes because authelia refuses that combination outright — a
daemon receives an access token and never an id-token.
An assertion rejects redirect URIs on a machine client: they are not
harmlessly unused, they mean the author believed a browser was
involved.
Refs #3274.
This commit is contained in:
parent
cfc8f2b22c
commit
176d95c0f0
2 changed files with 92 additions and 12 deletions
|
|
@ -99,17 +99,43 @@ let
|
|||
# the script, so nothing secret ever enters a nix expression (and
|
||||
# therefore the store). Everything else here is public metadata that
|
||||
# nix is the right place for.
|
||||
renderClient = c: ''
|
||||
printf -- ' - client_id: %s\n' ${lib.escapeShellArg c.id}
|
||||
printf -- ' client_name: %s\n' ${lib.escapeShellArg c.description}
|
||||
printf -- " client_secret: '%s'\n" "$(cat ${lib.escapeShellArg "${clientsDir}/${c.id}.digest"})"
|
||||
printf -- ' authorization_policy: one_factor\n'
|
||||
printf -- ' scopes: [openid, profile, email, groups]\n'
|
||||
printf -- ' redirect_uris:\n'
|
||||
${lib.concatMapStrings (u: ''
|
||||
printf -- ' - %s\n' ${lib.escapeShellArg u}
|
||||
'') c.redirectUris}
|
||||
'';
|
||||
# A machine client is not an interactive one with the redirect list left
|
||||
# empty: authelia derives the permitted grant from what is declared, and an
|
||||
# omitted `grant_types` means authorization-code ONLY. Measured against
|
||||
# authelia 4.39.20 — a client rendered without it answers a
|
||||
# `client_credentials` request with
|
||||
# unauthorized_client: The OAuth 2.0 Client is not allowed to use
|
||||
# authorization grant 'client_credentials'
|
||||
# so the two shapes have to be told apart here rather than inferred from an
|
||||
# empty list.
|
||||
#
|
||||
# `openid` is deliberately absent from a machine client's scopes: authelia
|
||||
# REFUSES the combination outright ("the values 'openid' are not allowed"
|
||||
# with `client_credentials`), because a daemon receives an access token and
|
||||
# never an id-token. There is no user to identify.
|
||||
renderClient =
|
||||
c:
|
||||
''
|
||||
printf -- ' - client_id: %s\n' ${lib.escapeShellArg c.id}
|
||||
printf -- ' client_name: %s\n' ${lib.escapeShellArg c.description}
|
||||
printf -- " client_secret: '%s'\n" "$(cat ${lib.escapeShellArg "${clientsDir}/${c.id}.digest"})"
|
||||
printf -- ' authorization_policy: one_factor\n'
|
||||
''
|
||||
+ (
|
||||
if c.kind == "machine" then
|
||||
''
|
||||
printf -- ' grant_types: ["client_credentials"]\n'
|
||||
printf -- ' scopes: []\n'
|
||||
''
|
||||
else
|
||||
''
|
||||
printf -- ' scopes: [openid, profile, email, groups]\n'
|
||||
printf -- ' redirect_uris:\n'
|
||||
${lib.concatMapStrings (u: ''
|
||||
printf -- ' - %s\n' ${lib.escapeShellArg u}
|
||||
'') c.redirectUris}
|
||||
''
|
||||
);
|
||||
|
||||
# The OIDC half of the first-boot generator, kept out of the script
|
||||
# body so neither is read through the other's indentation.
|
||||
|
|
@ -305,13 +331,44 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
kind = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"interactive"
|
||||
"machine"
|
||||
];
|
||||
default = "interactive";
|
||||
example = "machine";
|
||||
description = ''
|
||||
Whether a human logs in through this client, or a daemon
|
||||
authenticates as itself.
|
||||
|
||||
`interactive` is the authorization-code flow: a browser is
|
||||
redirected, a person authenticates, the client receives an
|
||||
id-token. `machine` is `client_credentials`: there is
|
||||
nobody to redirect and no identity to assert but the
|
||||
client's own, so it receives an access token and no
|
||||
id-token.
|
||||
|
||||
This is declared rather than inferred from an empty
|
||||
`redirectUris`, because authelia permits only the grants a
|
||||
client names — omitting `grant_types` yields
|
||||
authorization-code alone, and a daemon then fails at the
|
||||
token endpoint with `unauthorized_client` rather than at
|
||||
evaluation.
|
||||
'';
|
||||
};
|
||||
|
||||
redirectUris = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "https://forge.example.com/user/oauth2/authelia/callback" ];
|
||||
description = ''
|
||||
Exact callback URLs the provider will redirect to.
|
||||
Matched literally by authelia — a trailing-slash
|
||||
difference is a rejected login, not a warning.
|
||||
|
||||
Meaningless for `kind = "machine"`, which is asserted
|
||||
rather than silently ignored.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
|
@ -403,6 +460,19 @@ in
|
|||
};
|
||||
|
||||
config = lib.mkIf (hyperhiveCfg.enable && cfg.enable) {
|
||||
# A redirect URI on a machine client is not harmless-but-unused: it
|
||||
# means whoever wrote it believes a browser is involved. Failing here
|
||||
# is how that belief gets corrected at the point it was expressed,
|
||||
# rather than at a token endpoint months later.
|
||||
assertions = map (c: {
|
||||
assertion = c.kind != "machine" || c.redirectUris == [ ];
|
||||
message =
|
||||
"services.hyperhive.swarm.authelia.oidc.clients: client '${c.id}' is "
|
||||
+ "kind = \"machine\" but declares redirectUris. A client_credentials "
|
||||
+ "client has nobody to redirect; drop the URIs or make it "
|
||||
+ "kind = \"interactive\".";
|
||||
}) cfg.oidc.clients;
|
||||
|
||||
# Authelia's own gateway surface: the vhost that fronts it and the
|
||||
# name the hive resolver answers for. Both live here rather than in
|
||||
# the gateway, and both are inside `cfg.enable` — that guard is the
|
||||
|
|
|
|||
|
|
@ -240,11 +240,21 @@ in
|
|||
# queue's clients are daemons with nobody to redirect, so a
|
||||
# non-interactive grant is what makes a hive able to authenticate at
|
||||
# all. No redirect URI exists or is wanted.
|
||||
#
|
||||
# ⚠️ `kind` says that, and an empty `redirectUris` does NOT. This
|
||||
# declaration carried the comment above while rendering as an
|
||||
# interactive client, because authelia permits only the grants a
|
||||
# client names and an omitted `grant_types` means authorization-code
|
||||
# alone. Measured against 4.39.20: the token endpoint answered
|
||||
# `unauthorized_client: The OAuth 2.0 Client is not allowed to use
|
||||
# authorization grant 'client_credentials'`. Introspection — which
|
||||
# is all the responder needs today — worked throughout, which is why
|
||||
# nothing was visibly broken while the comment was untrue.
|
||||
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf autheliaCfg.enable [
|
||||
{
|
||||
id = cfg.clientId;
|
||||
description = "HyperHive swarm queue";
|
||||
redirectUris = [ ];
|
||||
kind = "machine";
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue