Skip to content

Commit

Permalink
Merge pull request #12827 from markylaing/cache-oidc-identities
Browse files Browse the repository at this point in the history
Auth: Add OIDC identities to identity cache and extract identity provider groups
  • Loading branch information
tomponline authored Feb 12, 2024
2 parents 09c384a + 458b87c commit adfaa52
Show file tree
Hide file tree
Showing 16 changed files with 266 additions and 75 deletions.
29 changes: 18 additions & 11 deletions client/lxd_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ func (r *ProtocolLXD) setupOIDCClient(token *oidc.Tokens[*oidc.IDTokenClaims]) {
r.oidcClient.httpClient = r.http
}

// Custom transport that modifies requests to inject the audience field.
// oidcTransport is a custom HTTP transport that injects the audience field into requests directed at the device
// authorization endpoint.
type oidcTransport struct {
deviceAuthorizationEndpoint string
audience string
}

// oidcTransport is a custom HTTP transport that injects the audience field into requests directed at the device authorization endpoint.
// RoundTrip is a method of oidcTransport that modifies the request, adds the audience parameter if appropriate, and sends it along.
// RoundTrip the oidcTransport implementation of http.RoundTripper. It modifies the request, adds the audience parameter
// if appropriate, and sends it along.
func (o *oidcTransport) RoundTrip(r *http.Request) (*http.Response, error) {
// Don't modify the request if it's not to the device authorization endpoint, or there are no
// URL parameters which need to be set.
Expand Down Expand Up @@ -113,10 +114,11 @@ func (o *oidcClient) do(req *http.Request) (*http.Response, error) {
issuer := resp.Header.Get("X-LXD-OIDC-issuer")
clientID := resp.Header.Get("X-LXD-OIDC-clientid")
audience := resp.Header.Get("X-LXD-OIDC-audience")
groupsClaim := resp.Header.Get("X-LXD-OIDC-groups-claim")

err = o.refresh(issuer, clientID)
err = o.refresh(issuer, clientID, groupsClaim)
if err != nil {
err = o.authenticate(issuer, clientID, audience)
err = o.authenticate(issuer, clientID, audience, groupsClaim)
if err != nil {
return nil, err
}
Expand All @@ -135,7 +137,7 @@ func (o *oidcClient) do(req *http.Request) (*http.Response, error) {

// getProvider initializes a new OpenID Connect Relying Party for a given issuer and clientID.
// The function also creates a secure CookieHandler with random encryption and hash keys, and applies a series of configurations on the Relying Party.
func (o *oidcClient) getProvider(issuer string, clientID string) (rp.RelyingParty, error) {
func (o *oidcClient) getProvider(issuer string, clientID string, groupsClaim string) (rp.RelyingParty, error) {
hashKey := make([]byte, 16)
encryptKey := make([]byte, 16)

Expand All @@ -157,7 +159,12 @@ func (o *oidcClient) getProvider(issuer string, clientID string) (rp.RelyingPart
rp.WithHTTPClient(o.httpClient),
}

provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, "", "", oidcScopes, options...)
scopes := oidcScopes
if groupsClaim != "" {
scopes = append(oidcScopes, groupsClaim)
}

provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, "", "", scopes, options...)
if err != nil {
return nil, err
}
Expand All @@ -167,12 +174,12 @@ func (o *oidcClient) getProvider(issuer string, clientID string) (rp.RelyingPart

// 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) error {
func (o *oidcClient) refresh(issuer string, clientID string, groupsClaim string) error {
if o.tokens.Token == nil || o.tokens.RefreshToken == "" {
return errRefreshAccessToken
}

provider, err := o.getProvider(issuer, clientID)
provider, err := o.getProvider(issuer, clientID, groupsClaim)
if err != nil {
return errRefreshAccessToken
}
Expand All @@ -196,7 +203,7 @@ func (o *oidcClient) refresh(issuer string, clientID string) error {
// authenticate initiates the OpenID Connect device flow authentication process for the client.
// It presents a user code for the end user to input in the device that has web access and waits for them to complete the authentication,
// subsequently updating the client's tokens upon successful authentication.
func (o *oidcClient) authenticate(issuer string, clientID string, audience string) error {
func (o *oidcClient) authenticate(issuer string, clientID string, audience string, groupsClaim string) error {
// Store the old transport and restore it in the end.
oldTransport := o.httpClient.Transport
o.oidcTransport.audience = audience
Expand All @@ -206,7 +213,7 @@ func (o *oidcClient) authenticate(issuer string, clientID string, audience strin
o.httpClient.Transport = oldTransport
}()

provider, err := o.getProvider(issuer, clientID)
provider, err := o.getProvider(issuer, clientID, groupsClaim)
if err != nil {
return err
}
Expand Down
6 changes: 6 additions & 0 deletions doc/api-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2322,3 +2322,9 @@ mounted.

The API extension adds indication whether the LXD version is an LTS release.
This is indicated when command `lxc version` is executed or when `/1.0` endpoint is queried.

## `oidc_groups_claim`

This API extension enables setting an `oidc.groups.claim` configuration key.
If OIDC authentication is configured and this claim is set, LXD will request this claim in the scope of OIDC flow.
The value of the claim will be extracted and might be used to make authorization decisions.
10 changes: 10 additions & 0 deletions doc/config_options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1760,6 +1760,16 @@ This value is required by some providers.

```

```{config:option} oidc.groups.claim server-oidc
:scope: "global"
:shortdesc: "Expected audience value for the application"
:type: "string"
Specify a custom claim to be requested when performing OIDC flows.
Configure a corresponding custom claim in your identity provider and
add organization level groups to it. These can be mapped to LXD groups
for automatic access control.
```

```{config:option} oidc.issuer server-oidc
:scope: "global"
:shortdesc: "OpenID Connect Discovery URL for the provider"
Expand Down
8 changes: 4 additions & 4 deletions lxd/api_1.0.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ func api10Get(d *Daemon, r *http.Request) response.Response {
// Get the authentication methods.
authMethods := []string{api.AuthenticationMethodTLS}

oidcIssuer, oidcClientID, _ := s.GlobalConfig.OIDCServer()
oidcIssuer, oidcClientID, _, _ := s.GlobalConfig.OIDCServer()
if oidcIssuer != "" && oidcClientID != "" {
authMethods = append(authMethods, api.AuthenticationMethodOIDC)
}
Expand Down Expand Up @@ -841,7 +841,7 @@ func doAPI10UpdateTriggers(d *Daemon, nodeChanged, clusterChanged map[string]str
acmeCAURLChanged = true
case "acme.domain":
acmeDomainChanged = true
case "oidc.issuer", "oidc.client.id", "oidc.audience":
case "oidc.issuer", "oidc.client.id", "oidc.audience", "oidc.groups.claim":
oidcChanged = true
}
}
Expand Down Expand Up @@ -985,13 +985,13 @@ func doAPI10UpdateTriggers(d *Daemon, nodeChanged, clusterChanged map[string]str
}

if oidcChanged {
oidcIssuer, oidcClientID, oidcAudience := clusterConfig.OIDCServer()
oidcIssuer, oidcClientID, oidcAudience, oidcGroupsClaim := clusterConfig.OIDCServer()

if oidcIssuer == "" || oidcClientID == "" {
d.oidcVerifier = nil
} else {
var err error
d.oidcVerifier, err = oidc.NewVerifier(oidcIssuer, oidcClientID, oidcAudience, s.ServerCert, nil)
d.oidcVerifier, err = oidc.NewVerifier(oidcIssuer, oidcClientID, oidcAudience, s.ServerCert, &oidc.Opts{GroupsClaim: oidcGroupsClaim})
if err != nil {
return fmt.Errorf("Failed creating verifier: %w", err)
}
Expand Down
Loading

0 comments on commit adfaa52

Please sign in to comment.