Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OIDC: Sessions #15030

Draft
wants to merge 47 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
82b3b31
api: Add `secret_lifetime` API extension.
markylaing Feb 17, 2025
bbbadd1
lxd/cluster/config: Add secret lifetime config keys.
markylaing Feb 17, 2025
1d5409a
doc: Update metadata
markylaing Feb 17, 2025
1488f9a
lxd/db/cluster/secret: Add secret package.
markylaing Feb 17, 2025
190e1b1
lxd/config: Ignore `volatile.secret.*` keys in config map.
markylaing Feb 17, 2025
4e1766f
lxd/cluster/config: Add method to get key and salt lifetimes.
markylaing Feb 17, 2025
ff45af4
lxd: Add `getClusterSecret` function to `Daemon`.
markylaing Feb 17, 2025
ff5b87e
lxd: Unset `(*Daemon).clusterSecretInternal` on cluster join.
markylaing Feb 17, 2025
6555775
lxd: Invalidate the key/salt when their lifetime is changed.
markylaing Feb 17, 2025
d970ddf
lxd/auth/oidc: Update Verifier to use cluster-wide secret.
markylaing Feb 17, 2025
79be02d
lxd/auth/oidc: Optionally pass host and context to NewVerifier.
markylaing Feb 17, 2025
8d628ad
lxd: Update calls to NewVerifier.
markylaing Feb 17, 2025
d632561
lxd/auth/oidc: Allow usage of older relying parties for /oidc/callback.
markylaing Feb 17, 2025
5d72a2b
test/suites: Test enforcement of issuer URL being compatible with dis…
markylaing Feb 17, 2025
4c7628a
api: Add `oidc_sessions` API extension.
markylaing Feb 19, 2025
1c5e998
gomod: Add dependencies.
markylaing Feb 19, 2025
d712351
lxd/auth/oidc: Add refresh token and session ID to authentication res…
markylaing Feb 19, 2025
2071226
lxd/auth/oidc: Add SessionHandler type.
markylaing Feb 19, 2025
911c55e
lxd/db/cluster: Add methods for getting identity info.
markylaing Feb 19, 2025
db85d2d
lxd/db/oidc: Add SessionHandler implementation.
markylaing Feb 19, 2025
ab869a5
lxd/cluster/config: Add `oidc.session.lifetime` config key.
markylaing Feb 19, 2025
eea8b64
doc: Update metadata.
markylaing Feb 19, 2025
01e63aa
lxd/auth/oidc: Update NewVerifier with session lifetime/handler and c…
markylaing Feb 19, 2025
ec0b4b6
lxd: Update OIDC config and NewVerifier calls.
markylaing Feb 19, 2025
911b058
lxd/auth/oidc: Move cookie and encryption handling to other files.
markylaing Feb 19, 2025
bcdef2c
lxd/auth/oidc: Update authorization code flow to set session cookie.
markylaing Feb 19, 2025
dfb6459
lxd/auth/oidc: Update IsRequest to check session cookie.
markylaing Feb 19, 2025
3c5a422
lxd/auth/oidc: Update OIDC Verifier to use sessions.
markylaing Feb 19, 2025
c7a1d15
lxd/identity: Strip the identity cache back to only contain certifica…
markylaing Feb 19, 2025
6709ed1
lxd: Change `updateIdentityCache` to only add certificates.
markylaing Feb 19, 2025
abbd34e
lxd: Update calls to identity cache.
markylaing Feb 19, 2025
f79f3ff
lxd: Update tests using the identity cache.
markylaing Feb 19, 2025
a6bd085
lxd/auth/drivers: Remove identity cache from authorizers.
markylaing Feb 19, 2025
078228a
lxd/auth: Remove identity cache from auth utils.
markylaing Feb 19, 2025
2339c22
lxd/request: Remove IdP groups context key and add identity info and …
markylaing Feb 19, 2025
a4b37bc
lxd/cluster: Don't send IdP groups header.
markylaing Feb 19, 2025
5f77555
lxd: Only update identity cache when a certificate changes.
markylaing Feb 19, 2025
357e9a3
lxd: Updates from auth util changes.
markylaing Feb 19, 2025
6eb0f39
lxd: Don't add identity cache to authorizer instantiations.
markylaing Feb 19, 2025
1e66ec6
lxd: Don't add IdP groups to request context.
markylaing Feb 19, 2025
ba0cc5a
lxd: Update Authenticate method and remove handle OIDC result method.
markylaing Feb 19, 2025
c15f09d
client: Don't always sent the OIDC access token.
markylaing Feb 19, 2025
e478526
client: Update the `do` method for the OIDC client.
markylaing Feb 19, 2025
21cbcb0
lxc/config: Add cookie handling to config.
markylaing Feb 19, 2025
9b9e9c8
lxc: Save cookies on exit, delete cookies on remote remove.
markylaing Feb 19, 2025
4864c5c
lxc: Ignore lint error (staticcheck).
markylaing Feb 19, 2025
3c8deab
test/godeps: Update deps for lxc config.
markylaing Feb 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions client/lxd.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,6 @@ func (r *ProtocolLXD) addClientHeaders(req *http.Request) {
if r.requireAuthenticated {
req.Header.Set("X-LXD-authenticated", "true")
}

if r.oidcClient != nil {
req.Header.Set("Authorization", "Bearer "+r.oidcClient.getAccessToken())
}
}

// RequireAuthenticated sets whether we expect to be authenticated with the server.
Expand Down Expand Up @@ -432,6 +428,12 @@ func (r *ProtocolLXD) rawWebsocket(url string) (*websocket.Conn, error) {
// Create temporary http.Request using the http url, not the ws one, so that we can add the client headers
// for the websocket request.
req := &http.Request{URL: &r.httpBaseURL, Header: http.Header{}}
if r.http.Jar != nil {
for _, cookie := range r.http.Jar.Cookies(req.URL) {
req.AddCookie(cookie)
}
}

r.addClientHeaders(req)

// Establish the connection
Expand Down
51 changes: 33 additions & 18 deletions client/lxd_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,24 @@ func newOIDCClient(tokens *oidc.Tokens[*oidc.IDTokenClaims]) *oidcClient {
return &client
}

// getAccessToken returns the Access Token from the oidcClient's tokens, or an empty string if no tokens are present.
func (o *oidcClient) getAccessToken() string {
if o.tokens == nil || o.tokens.Token == nil {
return ""
}

return o.tokens.AccessToken
}

// do function executes an HTTP request using the oidcClient's http client, and manages authorization by refreshing or authenticating as needed.
// If the request fails with an HTTP Unauthorized status, it attempts to refresh the access token, or perform an OIDC authentication if refresh fails.
// The oidcScopesExtensionPresent argument changes the behaviour of this function based on the presence of an API extension.
func (o *oidcClient) do(req *http.Request, oidcScopesExtensionPresent bool) (*http.Response, error) {
if o.httpClient.Jar == nil || len(o.httpClient.Jar.Cookies(req.URL)) == 0 {
// If there is no cookie, pre-emptively set the Authorization header so that LXD knows we're
// trying to authenticate with OIDC.
token := ""
if o.tokens != nil && o.tokens.Token != nil {
token = o.tokens.AccessToken
}

req.Header.Set("Authorization", "Bearer "+token)
}

// Clone the request so that, if we do send a cookie that becomes invalidated, we don't send it again.
// This is because it persists on the *http.Request but is unset on the *http.Client.
clonedReq := req.Clone(req.Context())
resp, err := o.httpClient.Do(req)
if err != nil {
return nil, err
Expand All @@ -113,6 +118,20 @@ func (o *oidcClient) do(req *http.Request, oidcScopesExtensionPresent bool) (*ht
return resp, nil
}

// If we have an access token, it might still be valid.
if o.tokens != nil && o.tokens.Token != nil && o.tokens.Token.AccessToken != "" {
clonedReq.Header.Set("Authorization", "Bearer "+o.tokens.AccessToken)
resp, err = o.httpClient.Do(clonedReq)
if err != nil {
return nil, err
}

// Return immediately if the error is not HTTP status unauthorized.
if resp.StatusCode != http.StatusUnauthorized {
return resp, nil
}
}

issuer := resp.Header.Get("X-LXD-OIDC-issuer")
clientID := resp.Header.Get("X-LXD-OIDC-clientid")
audience := resp.Header.Get("X-LXD-OIDC-audience")
Expand All @@ -139,23 +158,19 @@ func (o *oidcClient) do(req *http.Request, oidcScopesExtensionPresent bool) (*ht
}
}

// Try to refresh (returns an error if no refresh token present)
err = o.refresh(issuer, clientID, scopes)
if err != nil {
// Otherwise authenticate
err = o.authenticate(issuer, clientID, audience, scopes)
if err != nil {
return nil, err
}
}

// Set the new access token in the header.
req.Header.Set("Authorization", "Bearer "+o.tokens.AccessToken)

resp, err = o.httpClient.Do(req)
if err != nil {
return nil, err
}

return resp, nil
clonedReq.Header.Set("Authorization", "Bearer "+o.tokens.AccessToken)
return o.httpClient.Do(clonedReq)
}

// getProvider initializes a new OpenID Connect Relying Party for a given issuer and clientID.
Expand Down Expand Up @@ -196,7 +211,7 @@ func (o *oidcClient) getProvider(issuer string, clientID string, scopes []string
// refresh attempts to refresh the OpenID Connect access token for the client using the refresh token.
// If no token is present or the refresh token is empty, it returns an error. If successful, it updates the access token and other relevant token fields.
func (o *oidcClient) refresh(issuer string, clientID string, scopes []string) error {
if o.tokens.Token == nil || o.tokens.RefreshToken == "" {
if o.tokens == nil || o.tokens.Token == nil || o.tokens.RefreshToken == "" {
return errRefreshAccessToken
}

Expand Down
24 changes: 24 additions & 0 deletions doc/api-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2618,3 +2618,27 @@ Note that the `openid` and `email` scopes are always required.
## `project_default_network_and_storage`

Adds flags --network and --storage. The --network flag adds a network device connected to the specified network to the default profile. The --storage flag adds a root disk device using the specified storage pool to the default profile.

## `secret_lifetime`

Cookie encryption keys used for OIDC authentication are derived from a secret key using a salt.
This API extension enables setting the {config:option}`server-core:core.secret_key_lifetime` and {config:option}`server-core:core.salt_lifetime` configuration keys.

When the key or salt is required, LXD will check if it has expired.
If either have expired, a new salt or key will be created with the given lifetime.

The salt lifetime should be shorter than the key lifetime.
To reflect this, the key lifetime is specified in days and the salt lifetime is specified in minutes.

When the key rotates, all OIDC users will be logged out of LXD.
If they are still logged into the identity provider, this will cause a brief redirection.
Otherwise, they will need to log back in to the identity provider.

## `oidc_sessions`

This API extension adds server-side sessions to LXD for OpenID Connect (OIDC) authenticated identities.
The {config:option}`server-oidc:oidc.session.lifetime` configuration key can be used to set the session duration.
When a session expires, LXD will confirm the users authentication status with the identity provider (IdP).
If the `offline_access` scope is requested, a new token will be retrieved from the IdP.
Otherwise, the user will need to re-authenticate with the IdP.
Note that if the user is already logged into the IdP then only a brief redirect will occur.
40 changes: 40 additions & 0 deletions doc/metadata.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4594,6 +4594,35 @@ If this option is not specified, LXD falls back to the `NO_PROXY` environment va

```

```{config:option} core.salt_lifetime server-core
:defaultdesc: "`60`"
:scope: "global"
:shortdesc: "The lifetime in minutes of the cluster salt."
:type: "integer"
The cluster salt is used to encrypt cookies that are used to verify the integrity of the OpenID Connect (OIDC) browser login flow.
This configuration specifies the number of minutes a given salt is valid for.

The default value is 60 minutes.
```

```{config:option} core.secret_key_lifetime server-core
:defaultdesc: "`30`"
:scope: "global"
:shortdesc: "The lifetime in days of the cluster secret key."
:type: "integer"
The cluster secret key is used to encrypt cookies that are used to verify the integrity of the OpenID Connect (OIDC) browser login flow.
It is also used to encrypt cookies used for maintaining login information for OIDC authenticated users.
This configuration specifies the number of days a given key is valid for.
When a key is required, LXD checks if the current key has expired.
If the key has expired, a new key is generated with the given lifetime in days.
The default value is 30 days.

Note that this key is used in combination with a salt for obfuscation.

When this key rotates, all users that are logged in via OIDC will need to re-authenticate with the identity provider (IdP).
If they are still logged in with the IdP, they will be automatically logged back into LXD.
```

```{config:option} core.shutdown_timeout server-core
:defaultdesc: "`5`"
:scope: "global"
Expand Down Expand Up @@ -4884,6 +4913,17 @@ If you remove the `profile` scope, user information may not be displayed in LXD
You may add additional scopes if this is required by your identity provider, or if necessary for configuration of {ref}`identity provider groups <identity-provider-groups>`.
```

```{config:option} oidc.session.lifetime server-oidc
:defaultdesc: "`300`"
:scope: "global"
:shortdesc: "The lifetime in minutes each user session."
:type: "integer"
The lifetime in minutes of each user session.
The default value is 300 minutes (5 hours).
Note that when a session times out, the user will remain logged in if they are still logged in with the identity provider.
This value controls how frequently LXD checks authentication status with the identity provider.
```

<!-- config group server-oidc end -->
<!-- config group storage-btrfs-bucket-conf start -->
```{config:option} size storage-btrfs-bucket-conf
Expand Down
21 changes: 13 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ require (
github.com/go-acme/lego/v4 v4.22.2
github.com/go-chi/chi/v5 v5.2.1
github.com/go-jose/go-jose/v4 v4.0.4
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/gopacket v1.1.19
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/websocket v1.5.1
github.com/gosexy/gettext v0.0.0-20160830220431-74466a0a0c4a
github.com/j-keck/arping v1.0.3
github.com/jaypipes/pcidb v1.0.1
github.com/jochenvg/go-udev v0.0.0-20240801134859-b65ed646224b
github.com/juju/gomaasapi v0.0.0-20200602032615-aa561369c767
github.com/juju/persistent-cookiejar v1.0.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/lxc/go-lxc v0.0.0-20240606200241-27b3d116511f
github.com/mattn/go-colorable v0.1.14
Expand All @@ -40,8 +41,8 @@ require (
github.com/oklog/ulid/v2 v2.1.0
github.com/olekukonko/tablewriter v0.0.5
github.com/openfga/api/proto v0.0.0-20250127102726-f9709139a369
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20241115164311-10e575c8e47c
github.com/openfga/openfga v1.8.4
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250121233318-0eae96a39570
github.com/openfga/openfga v1.8.5
github.com/osrg/gobgp/v3 v3.34.0
github.com/pkg/sftp v1.13.7
github.com/pkg/xattr v0.4.10
Expand All @@ -55,7 +56,7 @@ require (
go.starlark.net v0.0.0-20250205221240-492d3672b3f4
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.33.0
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
golang.org/x/oauth2 v0.26.0
golang.org/x/sync v0.11.0
golang.org/x/sys v0.30.0
Expand Down Expand Up @@ -96,6 +97,7 @@ require (
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/cel-go v0.23.2 // indirect
github.com/google/renameio v1.0.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
Expand All @@ -106,12 +108,13 @@ require (
github.com/jkeiser/iter v0.0.0-20200628201005-c8aa0ae784d1 // indirect
github.com/juju/collections v1.0.4 // indirect
github.com/juju/errors v1.0.0 // indirect
github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a // indirect
github.com/juju/loggo v1.0.0 // indirect
github.com/juju/mgo/v2 v2.0.2 // indirect
github.com/juju/schema v1.2.0 // indirect
github.com/juju/version v0.0.0-20210303051006-2015802527a8 // indirect
github.com/k-sone/critbitgo v1.4.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/magiconair/properties v1.8.9 // indirect
Expand All @@ -130,7 +133,7 @@ require (
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_golang v1.21.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
Expand Down Expand Up @@ -164,11 +167,13 @@ require (
golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.35.0 // indirect
gonum.org/v1/gonum v0.15.1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250212204824-5a70512c5d8b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/grpc v1.70.0 // indirect
gopkg.in/errgo.v1 v1.0.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect
gopkg.in/retry.v1 v1.0.3 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
Loading