Skip to content

Commit 20b7225

Browse files
committed
x-pack/filebeat/input/{cel,httpjson,internal/dpop}: add support for DPoP OAuth for Okta
1 parent 65e1f2d commit 20b7225

File tree

11 files changed

+868
-14
lines changed

11 files changed

+868
-14
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ changelog/fragments/
136136
/x-pack/filebeat/input/gcs/ @elastic/security-service-integrations
137137
/x-pack/filebeat/input/http_endpoint/ @elastic/security-service-integrations
138138
/x-pack/filebeat/input/httpjson/ @elastic/security-service-integrations
139+
/x-pack/filebeat/input/internal/dpop @elastic/security-service-integrations
139140
/x-pack/filebeat/input/internal/httplog @elastic/security-service-integrations
140141
/x-pack/filebeat/input/internal/httpmon @elastic/security-service-integrations
141142
/x-pack/filebeat/input/internal/private @elastic/security-service-integrations
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
kind: feature
2+
summary: Add support for DPoP authentication for the CEL and HTTP JSON inputs.
3+
component: filebeat
4+
5+
# AUTOMATED
6+
# OPTIONAL to manually add other PR URLs
7+
# PR URL: A link the PR that added the changeset.
8+
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added.
9+
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number.
10+
# Please provide it if you are adding a fragment for a different PR.
11+
# pr: https://github.com/owner/repo/1234
12+
13+
# AUTOMATED
14+
# OPTIONAL to manually add other issue URLs
15+
# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
16+
# If not present is automatically filled by the tooling with the issue linked to the PR number.
17+
# issue: https://github.com/owner/repo/1234

docs/reference/filebeat/filebeat-input-cel.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,12 @@ Only one of the credentials settings can be set at once. For more information pl
715715

716716

717717

718+
### `auth.oauth2.okta.dpop_key_pem` [_auth_oauth2_okta_dpop_key_pem]
719+
720+
The Demonstrating Proof-of-Possession private key PEM block for your Okta authentication token. When this key is provided, Okta authentication will make use of the [Okta DPoP authentication flow](https://www.okta.com/blog/product-innovation/a-leap-forward-in-token-security-okta-adds-support-for-dpop/).
721+
722+
723+
718724
### `auth.token.enabled` [_auth_token_enabled]
719725

720726
When set to `false`, disables the token authentication configuration. Default: `true`.

docs/reference/filebeat/filebeat-input-httpjson.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,11 @@ The RSA JWK private key PEM block for your Okta Service App which is used for in
407407
Only one of the credentials settings can be set at once. For more information please refer to [https://developer.okta.com/docs/guides/implement-oauth-for-okta-serviceapp/main/](https://developer.okta.com/docs/guides/implement-oauth-for-okta-serviceapp/main/)
408408
::::
409409

410+
### `auth.oauth2.okta.dpop_key_pem` [_auth_oauth2_okta_dpop_key_pem]
411+
412+
The Demonstrating Proof-of-Possession private key PEM block for your Okta authentication token. When this key is provided, Okta authentication will make use of the [Okta DPoP authentication flow](https://www.okta.com/blog/product-innovation/a-leap-forward-in-token-security-okta-adds-support-for-dpop/).
413+
414+
410415

411416

412417
### `auth.oauth2.google.delegated_account` [_auth_oauth2_google_delegated_account_2]

x-pack/filebeat/input/cel/config_auth.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ type oAuth2Config struct {
175175
OktaJWKFile string `config:"okta.jwk_file"`
176176
OktaJWKJSON common.JSONBlob `config:"okta.jwk_json"`
177177
OktaJWKPEM string `config:"okta.jwk_pem"`
178+
DPoPKeyPEM string `config:"okta.dpop_key_pem"`
178179
}
179180

180181
// isEnabled returns true if the `enable` field is set to true in the yaml.
@@ -245,7 +246,7 @@ func (o *oAuth2Config) client(ctx context.Context, client *http.Client) (*http.C
245246
}
246247
return oauth2.NewClient(ctx, creds.TokenSource), nil
247248
case oAuth2ProviderOkta:
248-
return o.fetchOktaOauthClient(ctx, client)
249+
return o.fetchOktaOauthClient(ctx)
249250
default:
250251
return nil, errors.New("oauth2 client: unknown provider")
251252
}

x-pack/filebeat/input/cel/config_okta_auth.go

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package cel
77
import (
88
"bytes"
99
"context"
10+
"crypto"
1011
"crypto/rsa"
1112
"crypto/x509"
1213
"encoding/base64"
@@ -24,6 +25,8 @@ import (
2425
"github.com/golang-jwt/jwt/v5"
2526
"golang.org/x/oauth2"
2627
"golang.org/x/oauth2/clientcredentials"
28+
29+
"github.com/elastic/beats/v7/x-pack/filebeat/input/internal/dpop"
2730
)
2831

2932
// oktaTokenSource is a custom implementation of the oauth2.TokenSource interface.
@@ -37,7 +40,7 @@ type oktaTokenSource struct {
3740
}
3841

3942
// fetchOktaOauthClient fetches an OAuth2 client using the Okta JWK credentials.
40-
func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context, _ *http.Client) (*http.Client, error) {
43+
func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context) (*http.Client, error) {
4144
conf := &oauth2.Config{
4245
ClientID: o.ClientID,
4346
Scopes: o.Scopes,
@@ -46,11 +49,37 @@ func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context, _ *http.Client)
4649
},
4750
}
4851

52+
oauthCtx := ctx
53+
var (
54+
claim dpop.ClaimerFunc
55+
key crypto.Signer
56+
method jwt.SigningMethod
57+
)
58+
if o.DPoPKeyPEM != "" {
59+
claim = func() *jwt.RegisteredClaims {
60+
now := time.Now()
61+
return &jwt.RegisteredClaims{
62+
Audience: []string{conf.Endpoint.TokenURL},
63+
Issuer: conf.ClientID,
64+
Subject: conf.ClientID,
65+
IssuedAt: jwt.NewNumericDate(now),
66+
ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
67+
}
68+
}
69+
var err error
70+
key, err = pemPKCS8PrivateKey([]byte(o.DPoPKeyPEM))
71+
if err != nil {
72+
return nil, fmt.Errorf("failed to decode dpop signer: %w", err)
73+
}
74+
cli, err := dpop.NewTokenClient(claim, key, jwt.SigningMethodRS256, nil)
75+
oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, cli)
76+
}
77+
4978
var (
5079
oktaJWT string
5180
err error
5281
)
53-
if len(o.OktaJWKPEM) != 0 {
82+
if o.OktaJWKPEM != "" {
5483
oktaJWT, err = generateOktaJWTPEM(o.OktaJWKPEM, conf)
5584
if err != nil {
5685
return nil, fmt.Errorf("oauth2 client: error generating Okta JWT PEM: %w", err)
@@ -62,7 +91,7 @@ func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context, _ *http.Client)
6291
}
6392
}
6493

65-
token, err := exchangeForBearerToken(ctx, oktaJWT, conf)
94+
token, err := exchangeForBearerToken(oauthCtx, oktaJWT, conf)
6695
if err != nil {
6796
return nil, fmt.Errorf("oauth2 client: error exchanging Okta JWT for bearer token: %w", err)
6897
}
@@ -76,7 +105,9 @@ func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context, _ *http.Client)
76105
// reuse the tokenSource to refresh the token (automatically calls
77106
// the custom Token() method when token is no longer valid).
78107
client := oauth2.NewClient(ctx, oauth2.ReuseTokenSource(token, tokenSource))
79-
108+
if claim != nil {
109+
return dpop.NewResourceClient(claim, key, method, tokenSource, client)
110+
}
80111
return client, nil
81112
}
82113

@@ -167,20 +198,28 @@ func generateOktaJWTPEM(pemdata string, cnf *oauth2.Config) (string, error) {
167198
return signJWT(cnf, key)
168199
}
169200

170-
func pemPKCS8PrivateKey(pemdata []byte) (any, error) {
201+
func pemPKCS8PrivateKey(pemdata []byte) (crypto.Signer, error) {
171202
blk, rest := pem.Decode(pemdata)
172203
if rest := bytes.TrimSpace(rest); len(rest) != 0 {
173204
return nil, fmt.Errorf("PEM text has trailing data: %d bytes", len(rest))
174205
}
175206
if blk == nil {
176207
return nil, errors.New("no PEM data")
177208
}
178-
return x509.ParsePKCS8PrivateKey(blk.Bytes)
209+
key, err := x509.ParsePKCS8PrivateKey(blk.Bytes)
210+
if err != nil {
211+
return nil, err
212+
}
213+
signer, ok := key.(crypto.Signer)
214+
if !ok {
215+
return nil, fmt.Errorf("key is not a signer: %T", key)
216+
}
217+
return signer, nil
179218
}
180219

181220
// signJWT creates a JWT token using required claims and sign it with the
182221
// private key.
183-
func signJWT(cnf *oauth2.Config, key any) (string, error) {
222+
func signJWT(cnf *oauth2.Config, key crypto.Signer) (string, error) {
184223
now := time.Now()
185224
signed, err := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{
186225
Audience: []string{cnf.Endpoint.TokenURL},

x-pack/filebeat/input/httpjson/config_auth.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ type oAuth2Config struct {
117117
OktaJWKFile string `config:"okta.jwk_file"`
118118
OktaJWKJSON common.JSONBlob `config:"okta.jwk_json"`
119119
OktaJWKPEM string `config:"okta.jwk_pem"`
120+
DPoPKeyPEM string `config:"okta.dpop_key_pem"`
120121

121122
prepared *http.Client
122123
}
@@ -191,7 +192,7 @@ func (o *oAuth2Config) client(ctx context.Context, client *http.Client) (*http.C
191192
}
192193
return oauth2.NewClient(ctx, creds.TokenSource), nil
193194
case oAuth2ProviderOkta:
194-
return o.fetchOktaOauthClient(ctx, client)
195+
return o.fetchOktaOauthClient(ctx)
195196

196197
default:
197198
return nil, errors.New("oauth2 client: unknown provider")

x-pack/filebeat/input/httpjson/config_okta_auth.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package httpjson
77
import (
88
"bytes"
99
"context"
10+
"crypto"
1011
"crypto/rsa"
1112
"crypto/x509"
1213
"encoding/base64"
@@ -24,6 +25,8 @@ import (
2425
"github.com/golang-jwt/jwt/v5"
2526
"golang.org/x/oauth2"
2627
"golang.org/x/oauth2/clientcredentials"
28+
29+
"github.com/elastic/beats/v7/x-pack/filebeat/input/internal/dpop"
2730
)
2831

2932
// oktaTokenSource is a custom implementation of the oauth2.TokenSource interface.
@@ -37,7 +40,7 @@ type oktaTokenSource struct {
3740
}
3841

3942
// fetchOktaOauthClient fetches an OAuth2 client using the Okta JWK credentials.
40-
func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context, _ *http.Client) (*http.Client, error) {
43+
func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context) (*http.Client, error) {
4144
conf := &oauth2.Config{
4245
ClientID: o.ClientID,
4346
Scopes: o.Scopes,
@@ -46,11 +49,37 @@ func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context, _ *http.Client)
4649
},
4750
}
4851

52+
oauthCtx := ctx
53+
var (
54+
claim dpop.ClaimerFunc
55+
key crypto.Signer
56+
method jwt.SigningMethod
57+
)
58+
if o.DPoPKeyPEM != "" {
59+
claim = func() *jwt.RegisteredClaims {
60+
now := time.Now()
61+
return &jwt.RegisteredClaims{
62+
Audience: []string{conf.Endpoint.TokenURL},
63+
Issuer: conf.ClientID,
64+
Subject: conf.ClientID,
65+
IssuedAt: jwt.NewNumericDate(now),
66+
ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
67+
}
68+
}
69+
var err error
70+
key, err = pemPKCS8PrivateKey([]byte(o.DPoPKeyPEM))
71+
if err != nil {
72+
return nil, fmt.Errorf("failed to decode dpop signer: %w", err)
73+
}
74+
cli, err := dpop.NewTokenClient(claim, key, jwt.SigningMethodRS256, nil)
75+
oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, cli)
76+
}
77+
4978
var (
5079
oktaJWT string
5180
err error
5281
)
53-
if len(o.OktaJWKPEM) != 0 {
82+
if o.OktaJWKPEM != "" {
5483
oktaJWT, err = generateOktaJWTPEM(o.OktaJWKPEM, conf)
5584
if err != nil {
5685
return nil, fmt.Errorf("oauth2 client: error generating Okta JWT PEM: %w", err)
@@ -75,7 +104,9 @@ func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context, _ *http.Client)
75104
}
76105
// reuse the tokenSource to refresh the token (automatically calls the custom Token() method when token is no longer valid).
77106
client := oauth2.NewClient(ctx, oauth2.ReuseTokenSource(token, tokenSource))
78-
107+
if claim != nil {
108+
return dpop.NewResourceClient(claim, key, method, tokenSource, client)
109+
}
79110
return client, nil
80111
}
81112

@@ -165,15 +196,23 @@ func generateOktaJWTPEM(pemdata string, cnf *oauth2.Config) (string, error) {
165196
return signJWT(cnf, key)
166197
}
167198

168-
func pemPKCS8PrivateKey(pemdata []byte) (any, error) {
199+
func pemPKCS8PrivateKey(pemdata []byte) (crypto.Signer, error) {
169200
blk, rest := pem.Decode(pemdata)
170201
if rest := bytes.TrimSpace(rest); len(rest) != 0 {
171202
return nil, fmt.Errorf("PEM text has trailing data: %d bytes", len(rest))
172203
}
173204
if blk == nil {
174205
return nil, errors.New("no PEM data")
175206
}
176-
return x509.ParsePKCS8PrivateKey(blk.Bytes)
207+
key, err := x509.ParsePKCS8PrivateKey(blk.Bytes)
208+
if err != nil {
209+
return nil, err
210+
}
211+
signer, ok := key.(crypto.Signer)
212+
if !ok {
213+
return nil, fmt.Errorf("key is not a signer: %T", key)
214+
}
215+
return signer, nil
177216
}
178217

179218
// signJWT creates a JWT token using required claims and sign it with the private key.

0 commit comments

Comments
 (0)