Skip to content

Commit 38c7de4

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

File tree

9 files changed

+857
-14
lines changed

9 files changed

+857
-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

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)