[LXD] OpenID Connect authentication

Project LXD
Status Implemented
Author(s) @monstermunchkin
Approver(s) @stgraber @tomp
Release 5.13
Internal ID LX037

Abstract

This adds OpenID Connect as a new authentication method.

Rationale

LXD currently supports only tls and candid as authentication methods. The former uses certificates for authentication, and the latter requires a candid server which is a macaroon-based authentication service.

OpenID Connect (OIDC) is a simple identity layer on top of the OAuth 2.0 protocol. One of the advantages of OIDC is that it allows authentication without LXD having to store and manage users and passwords.

Specification

Design

The following new server configuration keys will be added:

  • oidc.issuer
  • oidc.client.id
  • oidc.url_params

The oidc.issuer key contains the URL of the server issuing the tokens. This for example could point to a Keycloak server.

Clients are applications and services that can request authentication of a user, and communicate with the issuer using the oidc.client.id. OAuth2 supports two different kinds of clients: confidential clients, and public clients. For confidential clients, a client secret is needed which also needs to be safe. Since we cannot ensure this (as our code is public), the LXD CLI will act as a public client.

The oidc.url_params configuration key is optional, and tells the client which URL parameters it needs to add when logging in. Some OIDC Providers require setting the audience URL parameter, otherwise they will issue an empty access token.

OIDC can be enabled by simply setting oidc.issuer and oidc.client.id.

If the client wants to communicate with the LXD server using OpenID, it sets the X-LXD-oidc HTTP header for every request. That way the server knows what kind of authentication to use.

LXD advertises the authentication methods it supports. The oidc value will be added to this list. oidc should be favored over candid which itself is favored over tls.

Adding a new remote

The user can add a new remote using lxc remote add <foo> --auth-type=oidc.

Authentication

If the client cannot be authenticated, the LXD server returns an error containing the issuer, client ID, and client secret. It also contains the type of error and can be one of the following:

  • authentication request
  • invalid token

The authentication request error means that the user needs to log in. The invalid token error most likely means that the token has expired, but it can also have a different reason. This error also contains the reason which can be one of these errors.

Using the issuer, client ID, and client secret, the user will be presented with a login page. On successful login, the client receives an access token, refresh token, and ID token. These are stored on disk.

If invalid token was returns by the LXD server, and the refresh token is still valid, a new access token will be retrieved without user interaction. Otherwise the login page will be presented again.

Once the tokens have been retrieved, the only one used for authentication by the LXD server is the access token. The others stay with the user and client. The ID token is not used for anything beyond this point, and the refresh token is for refreshing the access token.

The access token will be sent with every request using the Authorization HTTP header:

Authorization: Bearer <access_token>

Since the access token is a JSON Web Token (JWT), it can be decoded and validated offline (i.e. without having to call the OpenID Provider). The LXD server will do this on every request, and return an error if the validation fails.

Initial flow to get token

  • User tries adding remote with lxc remote add <remote> --auth-type=oidc
  • LXD server returns OIDC information (issuer and client ID)
  • User is asked to log in using the browser using the provided URL
  • Once logged in, the client does a code exchange in order to get the tokens (access token, refresh token, and ID token)
  • Client uses access token to access LXD

Valid access token

  • Client sets Authorization HTTP header with access token to access LXD

Expired access token but valid refresh token

  • Client sets Authorization HTTP header with access token to access LXD
  • LXD returns OIDC information together with the invalid token error
  • Client gets a new access token using the refresh token
  • Client uses new access token to access LXD

Expired access token and expired refresh token

  • Client sets Authorization HTTP header with access token to access LXD
  • LXD returns OIDC information together with the invalid token error
  • Client fails to get a new access token using the expired refresh token
  • User is required to log in from the browser, and client uses OIDC information to get new tokens
  • Client uses new access token to access LXD

CLI changes

The --auth-type flag for lxc remote add accepts oidc.

Upgrade handling

If there’s a remote configured which uses candid, and the LXD server adds OIDC support, the client will continue using candid. OIDC should only then be used when adding a new remote.

Further information

OIDC clients use scope values to specify what access privileges are being requested for access tokens. The oidc scope is required when using OIDC authentication. The offline_access scope is used for getting refresh tokens. The LXD client will therefore use both of these scopes.

4 Likes

Please could you expand a little on what this is and where will it be stored? Thanks

A few comments:

  • Should switch config keys to OIDC
  • Should add a short description of what each of the server side config keys do (for those unfamiliar)
  • Add a simple bullet point list of what is going to happen when:
    • User first connects to an OIDC enabled LXD (initial flow to get token)
    • User connects to LXD later on (valid token)
    • User connects to LXD with an expired token but valid refresh token (refresh)
    • User connects to LXD with an expired token and no valid refresh token
  • Should clarify authentication support in LXD. We currently advertise whether we have tls or candid, oidc will be added to that, on remote add, oidc should be favored over candid which itself is favored over tls
  • Need to validate that this will all work with our other tools, lxd-migrate comes to mind
  • Add logic in upgrade handling section for detecting existing configured remotes in the client and keep those using candid if they’re setup for it, we should only automatically use oidc when adding a new remote
1 Like

Are there any local or DB storage requirements beyond the server configuration keys already mentioned?

No, there are no DB related changes needed for this.

Isn’t there a bit of a security concern with effectively anyone being able to get the client ID and client secret?

I did some reading regarding this, and you’re right. OAuth2 has the concepts of confidential clients and public clients.

Taken from here:

Confidential clients are applications that are able to securely authenticate
with the authorization server, for example being able to keep their registered
client secret safe.

Public clients are unable to use registered client secrets, such as applications
running in a browser or on a mobile device.

Since we cannot safely store the client secret, we need to drop it, and be a public client.

I think that a sequence diagram might help the reader better understand the intended flow and the information exchange between the different actors. For instance I cannot easily visualise the Initial flow to get token

Under the assumption that in this document we are looking at a cli based authentication flow I think we should be looking at a device authorisation grant flow (e.g. what you use to log in to your Netflix app on the tv)

The document (or documentation) should clarify which claims/scopes do we need. In addition to the standard ones would it make sense to ask the identity provider to provide us group membership information? Should we give an option to restrict access based on that?

While Keycloak is a good place to start I think that the solution should be tested using Azure AD or Okta as they are the identity providers used by the vast majority of large corporations. (we have test tenants in Canonical)

1 Like

It is my understanding that eventually LXD will be adding OAuth-based permissions using scopes. A public client would not be capable of limiting the scopes a user can request. I think that (while public clients should be something supported) adding a confidential client option in which the CLI asks for the secret should also be a supported scenario.

Not 100% sure on this though, one for example when using keycloak with a public client, could develop a “middleman” server that LXD connects to that assigns the appropriate scopes to the authentication session, so it would be technically possible to impose scope limitations on a public client.

For now, I’ve updated the initial flow to get token section. A diagram might be helpful as well.

I had a look at this earlier today, and I agree. This seems much simpler, as the user doesn’t need to start a web server.

For now, I believe we only need the scopes openid (required for OIDC) and offline_access (in order to get refresh tokens).