-
Notifications
You must be signed in to change notification settings - Fork 5k
x-pack/filebeat/input/{cel,httpjson,internal/dpop}: add support for DPoP OAuth for Okta #47441
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| kind: feature | ||
| summary: Add support for DPoP authentication for the CEL and HTTP JSON inputs. | ||
| component: filebeat | ||
|
|
||
| # AUTOMATED | ||
| # OPTIONAL to manually add other PR URLs | ||
| # PR URL: A link the PR that added the changeset. | ||
| # If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added. | ||
| # NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number. | ||
| # Please provide it if you are adding a fragment for a different PR. | ||
| # pr: https://github.com/owner/repo/1234 | ||
|
|
||
| # AUTOMATED | ||
| # OPTIONAL to manually add other issue URLs | ||
| # Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of). | ||
| # If not present is automatically filled by the tooling with the issue linked to the PR number. | ||
| # issue: https://github.com/owner/repo/1234 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ package cel | |
| import ( | ||
| "bytes" | ||
| "context" | ||
| "crypto" | ||
| "crypto/rsa" | ||
| "crypto/x509" | ||
| "encoding/base64" | ||
|
|
@@ -24,6 +25,8 @@ import ( | |
| "github.com/golang-jwt/jwt/v5" | ||
| "golang.org/x/oauth2" | ||
| "golang.org/x/oauth2/clientcredentials" | ||
|
|
||
| "github.com/elastic/beats/v7/x-pack/filebeat/input/internal/dpop" | ||
| ) | ||
|
|
||
| // oktaTokenSource is a custom implementation of the oauth2.TokenSource interface. | ||
|
|
@@ -37,7 +40,7 @@ type oktaTokenSource struct { | |
| } | ||
|
|
||
| // fetchOktaOauthClient fetches an OAuth2 client using the Okta JWK credentials. | ||
| func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context, _ *http.Client) (*http.Client, error) { | ||
| func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context) (*http.Client, error) { | ||
| conf := &oauth2.Config{ | ||
| ClientID: o.ClientID, | ||
| Scopes: o.Scopes, | ||
|
|
@@ -46,11 +49,40 @@ func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context, _ *http.Client) | |
| }, | ||
| } | ||
|
|
||
| oauthCtx := ctx | ||
| var ( | ||
| claim dpop.ClaimerFunc | ||
| key crypto.Signer | ||
| method jwt.SigningMethod | ||
| ) | ||
| if o.DPoPKeyPEM != "" { | ||
| claim = func() *jwt.RegisteredClaims { | ||
| now := time.Now() | ||
| return &jwt.RegisteredClaims{ | ||
| Audience: []string{conf.Endpoint.TokenURL}, | ||
| Issuer: conf.ClientID, | ||
| Subject: conf.ClientID, | ||
| IssuedAt: jwt.NewNumericDate(now), | ||
| ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)), | ||
| } | ||
| } | ||
| var err error | ||
| key, err = pemPKCS8PrivateKey([]byte(o.DPoPKeyPEM)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to decode dpop signer: %w", err) | ||
| } | ||
| cli, err := dpop.NewTokenClient(claim, key, jwt.SigningMethodRS256, nil) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to make token client: %w", err) | ||
| } | ||
| oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, cli) | ||
| } | ||
|
|
||
| var ( | ||
| oktaJWT string | ||
| err error | ||
| ) | ||
| if len(o.OktaJWKPEM) != 0 { | ||
| if o.OktaJWKPEM != "" { | ||
| oktaJWT, err = generateOktaJWTPEM(o.OktaJWKPEM, conf) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("oauth2 client: error generating Okta JWT PEM: %w", err) | ||
|
|
@@ -62,7 +94,7 @@ func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context, _ *http.Client) | |
| } | ||
| } | ||
|
|
||
| token, err := exchangeForBearerToken(ctx, oktaJWT, conf) | ||
| token, err := exchangeForBearerToken(oauthCtx, oktaJWT, conf) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("oauth2 client: error exchanging Okta JWT for bearer token: %w", err) | ||
| } | ||
|
|
@@ -76,7 +108,9 @@ func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context, _ *http.Client) | |
| // reuse the tokenSource to refresh the token (automatically calls | ||
| // the custom Token() method when token is no longer valid). | ||
| client := oauth2.NewClient(ctx, oauth2.ReuseTokenSource(token, tokenSource)) | ||
|
|
||
| if claim != nil { | ||
| return dpop.NewResourceClient(claim, key, method, tokenSource, client) | ||
| } | ||
| return client, nil | ||
| } | ||
|
|
||
|
|
@@ -167,20 +201,28 @@ func generateOktaJWTPEM(pemdata string, cnf *oauth2.Config) (string, error) { | |
| return signJWT(cnf, key) | ||
| } | ||
|
|
||
| func pemPKCS8PrivateKey(pemdata []byte) (any, error) { | ||
| func pemPKCS8PrivateKey(pemdata []byte) (crypto.Signer, error) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need to do pemPKCS8PrivateKey([]byte(pemdata)).PublicKey() here ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no real change in logic here; the addition at the end is just a safer (non-panicky) assertion, but all the documented returned types from
This cannot be a problem if the returned value from |
||
| blk, rest := pem.Decode(pemdata) | ||
| if rest := bytes.TrimSpace(rest); len(rest) != 0 { | ||
| return nil, fmt.Errorf("PEM text has trailing data: %d bytes", len(rest)) | ||
| } | ||
| if blk == nil { | ||
| return nil, errors.New("no PEM data") | ||
| } | ||
| return x509.ParsePKCS8PrivateKey(blk.Bytes) | ||
| key, err := x509.ParsePKCS8PrivateKey(blk.Bytes) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| signer, ok := key.(crypto.Signer) | ||
| if !ok { | ||
| return nil, fmt.Errorf("key is not a signer: %T", key) | ||
| } | ||
| return signer, nil | ||
| } | ||
|
|
||
| // signJWT creates a JWT token using required claims and sign it with the | ||
| // private key. | ||
| func signJWT(cnf *oauth2.Config, key any) (string, error) { | ||
| func signJWT(cnf *oauth2.Config, key crypto.Signer) (string, error) { | ||
| now := time.Now() | ||
| signed, err := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{ | ||
| Audience: []string{cnf.Endpoint.TokenURL}, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.