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
|
# the script, so nothing secret ever enters a nix expression (and
|
||||||
# therefore the store). Everything else here is public metadata that
|
# therefore the store). Everything else here is public metadata that
|
||||||
# nix is the right place for.
|
# nix is the right place for.
|
||||||
renderClient = c: ''
|
# A machine client is not an interactive one with the redirect list left
|
||||||
printf -- ' - client_id: %s\n' ${lib.escapeShellArg c.id}
|
# empty: authelia derives the permitted grant from what is declared, and an
|
||||||
printf -- ' client_name: %s\n' ${lib.escapeShellArg c.description}
|
# omitted `grant_types` means authorization-code ONLY. Measured against
|
||||||
printf -- " client_secret: '%s'\n" "$(cat ${lib.escapeShellArg "${clientsDir}/${c.id}.digest"})"
|
# authelia 4.39.20 — a client rendered without it answers a
|
||||||
printf -- ' authorization_policy: one_factor\n'
|
# `client_credentials` request with
|
||||||
printf -- ' scopes: [openid, profile, email, groups]\n'
|
# unauthorized_client: The OAuth 2.0 Client is not allowed to use
|
||||||
printf -- ' redirect_uris:\n'
|
# authorization grant 'client_credentials'
|
||||||
${lib.concatMapStrings (u: ''
|
# so the two shapes have to be told apart here rather than inferred from an
|
||||||
printf -- ' - %s\n' ${lib.escapeShellArg u}
|
# empty list.
|
||||||
'') c.redirectUris}
|
#
|
||||||
'';
|
# `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
|
# The OIDC half of the first-boot generator, kept out of the script
|
||||||
# body so neither is read through the other's indentation.
|
# 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 {
|
redirectUris = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
example = [ "https://forge.example.com/user/oauth2/authelia/callback" ];
|
example = [ "https://forge.example.com/user/oauth2/authelia/callback" ];
|
||||||
description = ''
|
description = ''
|
||||||
Exact callback URLs the provider will redirect to.
|
Exact callback URLs the provider will redirect to.
|
||||||
Matched literally by authelia — a trailing-slash
|
Matched literally by authelia — a trailing-slash
|
||||||
difference is a rejected login, not a warning.
|
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) {
|
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
|
# Authelia's own gateway surface: the vhost that fronts it and the
|
||||||
# name the hive resolver answers for. Both live here rather than in
|
# name the hive resolver answers for. Both live here rather than in
|
||||||
# the gateway, and both are inside `cfg.enable` — that guard is the
|
# 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
|
# queue's clients are daemons with nobody to redirect, so a
|
||||||
# non-interactive grant is what makes a hive able to authenticate at
|
# non-interactive grant is what makes a hive able to authenticate at
|
||||||
# all. No redirect URI exists or is wanted.
|
# 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 [
|
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf autheliaCfg.enable [
|
||||||
{
|
{
|
||||||
id = cfg.clientId;
|
id = cfg.clientId;
|
||||||
description = "HyperHive swarm queue";
|
description = "HyperHive swarm queue";
|
||||||
redirectUris = [ ];
|
kind = "machine";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue