feature: the OpenID Provider has the client credentials and device authorization grants, public clients, and service accounts as resources with roles and scopes in JWT access tokens.
Release / release (push) Successful in 17s
Release / release (push) Successful in 17s
This commit is contained in:
1 parent
3ed79c7563
commit
3c4c5a8c32
15 files changed
+1572
-240
No files matched your search
@@ -375,7 +375,7 @@ refused request exits with status 2, any other failure with 1.
|
||||
## Bash completion
|
||||
|
||||
Bash completes the commands, options and arguments of `vpn-router-user`, `vpn-router-group`,
|
||||
`vpn-router-token`, `vpn-config-edit`, `vpn-router-backup`, `vpn-router-ca`, `vpn-router-caddy`
|
||||
`vpn-router-role`, `vpn-router-token`, `vpn-config-edit`, `vpn-router-backup`, `vpn-router-ca`, `vpn-router-caddy`
|
||||
and `vpn-router-site` with the Tab key. The package installs a file per command in
|
||||
`/usr/share/bash-completion/completions`, which the `bash-completion` package, recommended by
|
||||
`vpn-router`, loads on the first Tab after the command. For root, who can read them, the
|
||||
@@ -702,8 +702,9 @@ it is added:
|
||||
- a service account, a Manager-only account whose `vpnRouterScope` is `service`, which another
|
||||
system signs in with to use a router service, such as an OpenTelemetry Collector reading
|
||||
`/metrics`; it cannot sign in to `/auth`. Besides its name, it has a display name and a
|
||||
description, and a service account with redirect URIs is a client of the OpenID Provider (see
|
||||
OpenID Provider). `add --service` adds one, and the web service manages them at `/service-accounts`,
|
||||
description, a service account with redirect URIs or grants is a client of the OpenID Provider,
|
||||
and every one is a resource of it, with its scopes and roles (see OpenID Provider). `add
|
||||
--service` adds one, and the web service manages them at `/service-accounts`,
|
||||
not at `/users`. Internal service accounts are those the router creates and manages for its own
|
||||
services, display name and description included; at present that is the metrics service's, the
|
||||
account `metrics.service_account` names while metrics are enabled (see Metrics), which
|
||||
@@ -741,9 +742,9 @@ vpn-router-user otp-remove alice
|
||||
| `list` | The users with their kind, names and e-mail addresses, whether each is enabled or locked, has a certificate and is enrolled for TOTP, the number of passkeys and the console login |
|
||||
| `add <name> --first-name F --last-name L --email E` | Adds a VPN user and issues the certificate |
|
||||
| `add <name> [--email E]` | Adds a Manager-only account, with the e-mail address when given; one name without the other two options is refused |
|
||||
| `add <name> --service [--display-name N] [--description D] [--email E] [--redirect-uri U ...] [--post-logout-redirect-uri U ...] [--allowed-group G ...]` | Adds a service account, with its display name, description, e-mail address and OpenID Provider client settings when given; the names are refused |
|
||||
| `add <name> --service [--display-name N] [--description D] [--email E] [--redirect-uri U ...] [--post-logout-redirect-uri U ...] [--allowed-group G ...] [--grant G ...] [--public-client yes\|no] [--client-scope R:S ...] [--resource-scope S ...]` | Adds a service account, with its display name, description, e-mail address and OpenID Provider settings when given; the names are refused |
|
||||
| `update <name> [--first-name F] [--last-name L] [--email E]` | Changes the names or the e-mail address |
|
||||
| `update <name> [--display-name N] [--description D] [--email E] [--redirect-uri U ...] [--post-logout-redirect-uri U ...] [--allowed-group G ...]` | Changes the display name, the description, the e-mail address or the OpenID Provider client settings of a service account; `''` removes one; a repeated client option gives all values of its kind |
|
||||
| `update <name> [--display-name N] [--description D] [--email E] [--redirect-uri U ...] [--post-logout-redirect-uri U ...] [--allowed-group G ...] [--grant G ...] [--public-client yes\|no] [--client-scope R:S ...] [--resource-scope S ...]` | Changes the display name, the description, the e-mail address or the OpenID Provider settings of a service account; `''` removes one; a repeated option gives all values of its kind |
|
||||
| `passwd <name>` | Sets the password |
|
||||
| `disable <name>`, `enable <name>` | Locks the user out of OpenVPN and `/auth`, and lets the user in again |
|
||||
| `unlock <name>` | Ends a lockout after failed logins and clears the failure count; a disabled user stays disabled |
|
||||
@@ -855,9 +856,10 @@ A disabled or locked user is refused a passkey sign-in too.
|
||||
`vpn-router-group` manages the groups. A group has a name, with the rules of a user name, an
|
||||
optional description of at most 256 printable characters, and at least one member, each a user
|
||||
of the directory. The group `admins` grants the management of the router, and the allowed groups
|
||||
of a client of the OpenID Provider admit their members to it; other groups record membership
|
||||
only. Renaming a group renames it in the clients that allow it, and a group a client allows
|
||||
cannot be removed. The output, `--json`, `-h` and `--help` work as for `vpn-router-user`.
|
||||
of a client of the OpenID Provider admit their members to it, and a role of a resource assigned to
|
||||
a group is a role of its members; other groups record membership only. Renaming a group renames it
|
||||
in the clients that allow it and in its roles, a group a client allows cannot be removed, and
|
||||
removing a group takes it out of its roles. The output, `--json`, `-h` and `--help` work as for `vpn-router-user`.
|
||||
|
||||
```sh
|
||||
vpn-router-group add ops --member alice --member bob --description 'Operations team'
|
||||
@@ -1278,22 +1280,33 @@ curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
|
||||
| Request | Body | Answer |
|
||||
| --- | --- | --- |
|
||||
| `GET /service-accounts` | | `{"service_accounts": [{"name", "display_name", "description", "internal", "enabled", "locked", "email", "redirect_uris", "post_logout_redirect_uris", "allowed_groups", ...}, ...], "oidc": {"enabled", "issuer"}}`, each as `/users` shows a user, and the issuer of the OpenID Provider, `null` while it is disabled |
|
||||
| `POST /service-accounts` | `{"name", "password", "display_name"?, "description"?, "email"?, "redirect_uris"?, "post_logout_redirect_uris"?, "allowed_groups"?}` | 201 and the account |
|
||||
| `PUT /service-accounts/<name>` | any of `{"display_name", "description", "email", "redirect_uris", "post_logout_redirect_uris", "allowed_groups"}`; `""` removes one of the first three, `[]` the values of the others | the account |
|
||||
| `GET /service-accounts` | | `{"service_accounts": [{"name", "display_name", "description", "internal", "enabled", "locked", "email", "redirect_uris", "post_logout_redirect_uris", "allowed_groups", "grants", "public_client", "client_scopes", "resource_scopes", "roles", ...}, ...], "oidc": {"enabled", "issuer"}}`, each as `/users` shows a user, and the issuer of the OpenID Provider, `null` while it is disabled |
|
||||
| `POST /service-accounts` | `{"name", "password", "display_name"?, "description"?, "email"?, "redirect_uris"?, "post_logout_redirect_uris"?, "allowed_groups"?, "grants"?, "public_client"?, "client_scopes"?, "resource_scopes"?, "roles"?}` | 201 and the account |
|
||||
| `PUT /service-accounts/<name>` | any of `{"display_name", "description", "email", "redirect_uris", "post_logout_redirect_uris", "allowed_groups", "grants", "public_client", "client_scopes", "resource_scopes", "roles"}`; `""` removes one of the first three, `[]` the values of the lists, `false` the public client | the account |
|
||||
| `PUT /service-accounts/<name>/password` | `{"password": ...}` | the account |
|
||||
| `POST /service-accounts/<name>/disable` | | the account |
|
||||
| `POST /service-accounts/<name>/enable` | | the account |
|
||||
| `POST /service-accounts/<name>/unlock` | | the account |
|
||||
| `DELETE /service-accounts/<name>` | | `{"name", "removed": true}` |
|
||||
|
||||
A display name has at most 64 printable characters and a description at most 256. The client
|
||||
settings are lists of at most 16 values: URIs of `https`, or of `http` for `localhost`,
|
||||
`127.0.0.1` or `::1`, without a fragment and of at most 1024 characters, and names of existing
|
||||
groups (see OpenID Provider). The internal
|
||||
service accounts, `internal` true, are only listed: every other request on one gets 403, as the
|
||||
router manages them, their display name and description included. A failure is otherwise as for
|
||||
`/users`, with 404 also for a name that is not a service account.
|
||||
A display name has at most 64 printable characters and a description at most 256. The settings of
|
||||
the OpenID Provider (see OpenID Provider) are:
|
||||
|
||||
- **URIs and allowed groups:** lists of at most 16 values: URIs of `https`, or of `http` for
|
||||
`localhost`, `127.0.0.1` or `::1`, without a fragment and of at most 1024 characters, and names
|
||||
of existing groups;
|
||||
- **`grants`:** `client_credentials` and `device_code`; `public_client`, `true` or `false`;
|
||||
- **`client_scopes`:** at most 16 scopes of resources, `<resource>:<scope>`;
|
||||
- **`resource_scopes`:** at most 16 scope names of lowercase letters, digits, dots, underscores and
|
||||
hyphens;
|
||||
- **`roles`:** `[{"name", "description", "groups", "accounts"}]`, which replaces the account's roles:
|
||||
role names of letters, digits, dots, underscores and hyphens, and existing groups and accounts.
|
||||
|
||||
The internal service accounts, `internal` true, are only listed: every other request on one, and
|
||||
a role assigned to one, gets 403, as the router manages them, their display name and description
|
||||
included. A password change, disabling, unlocking and removing revoke the tokens the account was
|
||||
issued. A failure is otherwise as for `/users`, with 404 also for a name that is not a service
|
||||
account.
|
||||
|
||||
### The /account API
|
||||
|
||||
@@ -1521,22 +1534,33 @@ curl https://router.example.com:7444/.well-known/openid-configuration
|
||||
| Endpoint | Purpose |
|
||||
| --- | --- |
|
||||
| `GET /.well-known/openid-configuration` | The discovery document |
|
||||
| `GET /oidc/jwks` | The public key ID tokens are signed with |
|
||||
| `GET /oidc/jwks` | The public key ID tokens and access tokens are signed with |
|
||||
| `GET`, `POST /oidc/authorize` | The authorization code flow; the sign-in page while the browser has no session |
|
||||
| `POST /oidc/token` | Exchanges a code or a refresh token for tokens |
|
||||
| `POST /oidc/token` | Exchanges a code, a refresh token, a device code or a client's credentials for tokens |
|
||||
| `POST /oidc/device_authorization` | Begins the device authorization grant: a device code and a user code |
|
||||
| `GET`, `POST /oidc/device` | The page where a user enters a device's user code, signs in and allows the device |
|
||||
| `GET`, `POST /oidc/userinfo` | The claims of the user of an access token |
|
||||
| `GET /oidc/logout` | Ends the session (RP-initiated logout) |
|
||||
|
||||
A client is a service account with at least one redirect URI: its name is the `client_id` and its
|
||||
**Clients**
|
||||
|
||||
A client is a service account with a redirect URI or a grant. Its name is the `client_id` and its
|
||||
password the `client_secret`, sent with HTTP Basic authentication (`client_secret_basic`) or in
|
||||
the body of the token request (`client_secret_post`). The password is checked with a bind, so 5
|
||||
wrong secrets lock the client for 15 minutes. Its settings are:
|
||||
wrong secrets lock the client for 15 minutes. A public client sends only its `client_id` (`none`).
|
||||
Its settings are:
|
||||
|
||||
- redirect URIs, where the provider sends the browser back with a code, matched exactly; a URI
|
||||
uses `https`, or `http` for `localhost`, `127.0.0.1` or `::1`;
|
||||
- post-logout redirect URIs, where `/oidc/logout` may send the browser afterwards;
|
||||
- allowed groups: only their members may sign in to the client; without any, every user who is
|
||||
not a service account may.
|
||||
- **redirect URIs:** where the provider sends the browser back with a code; they give the client
|
||||
the authorization code grant. A URI uses `https`, or `http` for `localhost`, `127.0.0.1` or
|
||||
`::1`, and is matched exactly, but the port of an `http` one, which a native program chooses
|
||||
when it starts (RFC 8252);
|
||||
- **post-logout redirect URIs:** where `/oidc/logout` may send the browser afterwards;
|
||||
- **allowed groups:** only their members may sign in to the client; without any, every user who
|
||||
is not a service account may;
|
||||
- **grants:** `client_credentials` and `device_code`, besides the authorization code;
|
||||
- **public client:** the client has no secret, as a native program or a CLI, which cannot keep
|
||||
one; it cannot have `client_credentials`;
|
||||
- **client scopes:** the scopes of resources the client may ask for, `<resource>:<scope>`.
|
||||
|
||||
```sh
|
||||
echo 'cl1ent-s3cret' | vpn-router-user add 3f2b8c1e-9a4d-4e6f-8b2a-1c5d7e9f0a3b --service \
|
||||
@@ -1545,34 +1569,131 @@ echo 'cl1ent-s3cret' | vpn-router-user add 3f2b8c1e-9a4d-4e6f-8b2a-1c5d7e9f0a3b
|
||||
vpn-router-user update 3f2b8c1e-9a4d-4e6f-8b2a-1c5d7e9f0a3b --allowed-group ''
|
||||
```
|
||||
|
||||
The flow is the authorization code flow with PKCE, whose `code_challenge_method` must be `S256`;
|
||||
the implicit and hybrid flows, and the password and client credentials grants, are refused. A
|
||||
registered client is trusted, so a user is not asked for consent. A user signs in on the
|
||||
provider's page with the name, the password and, when enrolled, the TOTP code, or with a passkey
|
||||
(see Passkeys); failed sign-ins count towards the lockout, and `POST /oidc/login` takes at most
|
||||
10 calls a minute from one client address, as `/auth` does. A
|
||||
disabled, locked or removed user, a service account, and a user outside the client's allowed
|
||||
groups cannot sign in, and the codes and tokens already issued to such a user stop working. The
|
||||
sign-in starts a session, a cookie of the issuer's `/oidc/` path valid for 8 hours, so later
|
||||
requests of any client go on without the page; `prompt=login` and `max_age` ask for a new sign-in,
|
||||
and `prompt=none` answers `login_required` without a session.
|
||||
**Grants**
|
||||
|
||||
| Grant | Who signs in | Tokens |
|
||||
| --- | --- | --- |
|
||||
| `authorization_code` | A user, in the browser, with PKCE (`S256`) | Access, refresh and ID token |
|
||||
| `urn:ietf:params:oauth:grant-type:device_code` | A user, on another device | Access, refresh and ID token |
|
||||
| `client_credentials` | The service account itself, with its secret | Access token |
|
||||
| `refresh_token` | | A new access and refresh token |
|
||||
|
||||
The implicit and hybrid flows, the password grant and dynamic registration are refused. A
|
||||
registered client is trusted, so a user is not asked for consent, except for a device.
|
||||
|
||||
**Signing in**
|
||||
|
||||
- **The page:** a user signs in on the provider's page with the name, the password and, when
|
||||
enrolled, the TOTP code, or with a passkey (see Passkeys).
|
||||
- **Lockout and rate limit:** failed sign-ins count towards the lockout. `POST /oidc/login`,
|
||||
`/oidc/device` and `/oidc/device_authorization` take at most 10 calls a minute from one client
|
||||
address, as `/auth` does.
|
||||
- **Who may not sign in:** a disabled, locked or removed user, a service account, and a user
|
||||
outside the client's allowed groups. The codes and tokens already issued to such a user stop
|
||||
working.
|
||||
- **The session:** the sign-in starts a cookie of the issuer's `/oidc/` path, valid for 8 hours,
|
||||
so later requests of any client go on without the page. `prompt=login` and `max_age` ask for a
|
||||
new sign-in, and `prompt=none` answers `login_required` without a session.
|
||||
|
||||
**Device authorization (RFC 8628)**
|
||||
|
||||
A client with `device_code`, such as a CLI on a host without a browser, signs a user in like this:
|
||||
|
||||
1. It posts `client_id` (and its secret, unless it is public), `scope` and optionally `resource` to
|
||||
`/oidc/device_authorization`. The answer holds `device_code`, `user_code` (8 letters, shown as
|
||||
`XXXX-XXXX`), `verification_uri`, `verification_uri_complete`, `expires_in` (600 seconds) and
|
||||
`interval` (5 seconds).
|
||||
2. It shows the user the code and the URI, then polls `/oidc/token` with
|
||||
`grant_type=urn:ietf:params:oauth:grant-type:device_code` and `device_code`. Until the user
|
||||
decides, the answer is `authorization_pending`; a poll sooner than the interval answers
|
||||
`slow_down` and adds 5 seconds to it.
|
||||
3. The user opens the URI on any device, enters the code, signs in, and allows or denies the
|
||||
client, the resource and the scopes shown. The next poll answers the tokens, or
|
||||
`access_denied`; an expired code answers `expired_token`.
|
||||
|
||||
```sh
|
||||
curl -d client_id=backup-cli -d 'scope=openid profile' \
|
||||
https://router.example.com:7444/oidc/device_authorization
|
||||
```
|
||||
|
||||
**Client credentials**
|
||||
|
||||
A confidential client with `client_credentials` gets an access token as its own service account,
|
||||
without a user. The scopes are those of a resource, and the profile scopes give the service
|
||||
account's claims; `openid` is refused, and no refresh token is issued.
|
||||
|
||||
```sh
|
||||
curl -u backup-job:'j0b-s3cret' -d grant_type=client_credentials -d resource=s3 -d scope=s3:read \
|
||||
https://router.example.com:7444/oidc/token
|
||||
```
|
||||
|
||||
**Resources, roles and scopes**
|
||||
|
||||
Every service account is also a resource, an application that checks the router's tokens, as an
|
||||
API registration of Entra ID:
|
||||
|
||||
- **Scopes:** the resource scopes the account defines, such as `read`, which clients ask for as
|
||||
`<resource>:<scope>` (`s3:read`) when their client scopes allow them.
|
||||
- **Roles:** named roles, such as `Reader`, each assigned to groups, users and service accounts;
|
||||
`vpn-router-role` manages them.
|
||||
|
||||
```sh
|
||||
echo 's3-s3cret' | vpn-router-user add s3 --service --resource-scope read --resource-scope write
|
||||
vpn-router-role add s3 Reader --description 'Reads every bucket'
|
||||
vpn-router-role member-add s3 Reader --group backup
|
||||
vpn-router-user update backup-job --grant client_credentials --client-scope s3:read
|
||||
```
|
||||
|
||||
A request is for a resource when it names it with `resource` (RFC 8707), its name or
|
||||
`<issuer>/<name>`, or asks for its scopes; more than one resource, or one that is not a
|
||||
resource, is `invalid_target`. A scope the client may not ask for is `invalid_scope`. Without a
|
||||
resource, the tokens are for the client itself. The resource is kept with the code, the device code
|
||||
and the refresh token, and a token request naming another one is `invalid_target`.
|
||||
|
||||
| Command | What it does |
|
||||
| --- | --- |
|
||||
| `vpn-router-role list [<resource>]` | The roles with the groups and accounts they are assigned to |
|
||||
| `vpn-router-role add <resource> <role> [--description D]` | Add a role |
|
||||
| `vpn-router-role remove <resource> <role>` | Remove a role |
|
||||
| `vpn-router-role member-add <resource> <role> --group G\|--account A` | Assign the role |
|
||||
| `vpn-router-role member-remove <resource> <role> --group G\|--account A` | Take the role away |
|
||||
|
||||
A scope a client may ask for cannot be removed from its resource, nor the resource while a client
|
||||
may ask for its scopes. Removing a service account removes its roles and takes it out of the
|
||||
others; removing or renaming a group does the same in the roles.
|
||||
|
||||
**Tokens**
|
||||
|
||||
| Scope | Claims |
|
||||
| --- | --- |
|
||||
| `openid` (required) | `sub`, the `entryUUID` of the user's entry |
|
||||
| `openid` (required but by `client_credentials`, which refuses it) | `sub`, the `entryUUID` of the user's entry |
|
||||
| `profile` | `preferred_username` (the user name), `name`, `given_name`, `family_name` |
|
||||
| `email` | `email` |
|
||||
| `groups` | `groups`, the names of the user's groups |
|
||||
| `<resource>:<scope>` | None; the scope is in the access token's `scope` |
|
||||
|
||||
The claims of the scopes granted are in the ID token and in the answer of `/oidc/userinfo`. ID
|
||||
tokens are RS256 JWTs, valid for 5 minutes, with `auth_time` and `amr` (`pwd`, `pwd` and `otp`, or
|
||||
`hwk` and `user` for a passkey), signed with an RSA key of 3072 bits that the web service makes on
|
||||
its first run, `/etc/vpn-router/oidc.key` (root only), and named in the JWKS by its RFC 7638
|
||||
thumbprint. A code is valid for 1 minute and used once, an access token for 5 minutes, and a
|
||||
refresh token for 8 hours and used once: each refresh answers a new one with a new ID token. The
|
||||
codes, tokens and sessions are kept, as their SHA-256, in the database of the refresh tokens of
|
||||
`/auth`. Changing a user's password, disabling, unlocking or removing the user ends its sessions
|
||||
and revokes its codes and tokens.
|
||||
- **ID tokens:** RS256 JWTs for the client (`aud`), valid for 5 minutes, with the claims of the
|
||||
scopes, `auth_time`, `amr` (`pwd`, `pwd` and `otp`, or `hwk` and `user` for a passkey), and
|
||||
`roles`, the user's roles of the client itself. `/oidc/userinfo` answers the same claims.
|
||||
- **Access tokens:** RS256 JWTs of RFC 9068 (`typ` `at+jwt`), valid for 5 minutes, which a
|
||||
resource verifies with the JWKS. Their claims:
|
||||
- `iss`, `sub`, `iat`, `exp` and `jti`;
|
||||
- `aud`, the resource, or the client without one, and `client_id`, the client;
|
||||
- `scope`, the scopes granted;
|
||||
- `roles`, the roles of the resource the user or the service account holds, directly or by a
|
||||
group;
|
||||
- the claims of the scopes, and of a user `auth_time` and `amr`.
|
||||
- **Roles are read at every issue,** refresh included, so a role taken away is gone from the next
|
||||
token.
|
||||
- **The key:** tokens are signed with an RSA key of 3072 bits that the web service makes on its
|
||||
first run, `/etc/vpn-router/oidc.key` (root only), and named in the JWKS by its RFC 7638
|
||||
thumbprint.
|
||||
- **Lifetimes:** a code is valid for 1 minute and used once, a device code for 10 minutes, and a
|
||||
refresh token for 8 hours and used once: each refresh answers a new one.
|
||||
- **Storage and revocation:** the codes, device codes, tokens and sessions are kept, as their
|
||||
SHA-256, in the database of the refresh tokens of `/auth`. Changing the password of a user or a
|
||||
service account, disabling, unlocking or removing it ends its sessions and revokes its codes and
|
||||
tokens.
|
||||
|
||||
## Web sites
|
||||
|
||||
|
||||
Reference in new issue
Block a user