Skip to content

Commit 20f9be4

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

File tree

11 files changed

+875
-15
lines changed

11 files changed

+875
-15
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_2]
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: 49 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,40 @@ 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+
if err != nil {
76+
return nil, fmt.Errorf("failed to make token client: %w", err)
77+
}
78+
oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, cli)
79+
}
80+
4981
var (
5082
oktaJWT string
5183
err error
5284
)
53-
if len(o.OktaJWKPEM) != 0 {
85+
if o.OktaJWKPEM != "" {
5486
oktaJWT, err = generateOktaJWTPEM(o.OktaJWKPEM, conf)
5587
if err != nil {
5688
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)
6294
}
6395
}
6496

65-
token, err := exchangeForBearerToken(ctx, oktaJWT, conf)
97+
token, err := exchangeForBearerToken(oauthCtx, oktaJWT, conf)
6698
if err != nil {
6799
return nil, fmt.Errorf("oauth2 client: error exchanging Okta JWT for bearer token: %w", err)
68100
}
@@ -76,7 +108,9 @@ func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context, _ *http.Client)
76108
// reuse the tokenSource to refresh the token (automatically calls
77109
// the custom Token() method when token is no longer valid).
78110
client := oauth2.NewClient(ctx, oauth2.ReuseTokenSource(token, tokenSource))
79-
111+
if claim != nil {
112+
return dpop.NewResourceClient(claim, key, method, tokenSource, client)
113+
}
80114
return client, nil
81115
}
82116

@@ -167,20 +201,28 @@ func generateOktaJWTPEM(pemdata string, cnf *oauth2.Config) (string, error) {
167201
return signJWT(cnf, key)
168202
}
169203

170-
func pemPKCS8PrivateKey(pemdata []byte) (any, error) {
204+
func pemPKCS8PrivateKey(pemdata []byte) (crypto.Signer, error) {
171205
blk, rest := pem.Decode(pemdata)
172206
if rest := bytes.TrimSpace(rest); len(rest) != 0 {
173207
return nil, fmt.Errorf("PEM text has trailing data: %d bytes", len(rest))
174208
}
175209
if blk == nil {
176210
return nil, errors.New("no PEM data")
177211
}
178-
return x509.ParsePKCS8PrivateKey(blk.Bytes)
212+
key, err := x509.ParsePKCS8PrivateKey(blk.Bytes)
213+
if err != nil {
214+
return nil, err
215+
}
216+
signer, ok := key.(crypto.Signer)
217+
if !ok {
218+
return nil, fmt.Errorf("key is not a signer: %T", key)
219+
}
220+
return signer, nil
179221
}
180222

181223
// signJWT creates a JWT token using required claims and sign it with the
182224
// private key.
183-
func signJWT(cnf *oauth2.Config, key any) (string, error) {
225+
func signJWT(cnf *oauth2.Config, key crypto.Signer) (string, error) {
184226
now := time.Now()
185227
signed, err := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{
186228
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: 48 additions & 6 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,40 @@ 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+
if err != nil {
76+
return nil, fmt.Errorf("failed to make token client: %w", err)
77+
}
78+
oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, cli)
79+
}
80+
4981
var (
5082
oktaJWT string
5183
err error
5284
)
53-
if len(o.OktaJWKPEM) != 0 {
85+
if o.OktaJWKPEM != "" {
5486
oktaJWT, err = generateOktaJWTPEM(o.OktaJWKPEM, conf)
5587
if err != nil {
5688
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)
6294
}
6395
}
6496

65-
token, err := exchangeForBearerToken(ctx, oktaJWT, conf)
97+
token, err := exchangeForBearerToken(oauthCtx, oktaJWT, conf)
6698
if err != nil {
6799
return nil, fmt.Errorf("oauth2 client: error exchanging Okta JWT for bearer token: %w", err)
68100
}
@@ -75,7 +107,9 @@ func (o *oAuth2Config) fetchOktaOauthClient(ctx context.Context, _ *http.Client)
75107
}
76108
// reuse the tokenSource to refresh the token (automatically calls the custom Token() method when token is no longer valid).
77109
client := oauth2.NewClient(ctx, oauth2.ReuseTokenSource(token, tokenSource))
78-
110+
if claim != nil {
111+
return dpop.NewResourceClient(claim, key, method, tokenSource, client)
112+
}
79113
return client, nil
80114
}
81115

@@ -165,15 +199,23 @@ func generateOktaJWTPEM(pemdata string, cnf *oauth2.Config) (string, error) {
165199
return signJWT(cnf, key)
166200
}
167201

168-
func pemPKCS8PrivateKey(pemdata []byte) (any, error) {
202+
func pemPKCS8PrivateKey(pemdata []byte) (crypto.Signer, error) {
169203
blk, rest := pem.Decode(pemdata)
170204
if rest := bytes.TrimSpace(rest); len(rest) != 0 {
171205
return nil, fmt.Errorf("PEM text has trailing data: %d bytes", len(rest))
172206
}
173207
if blk == nil {
174208
return nil, errors.New("no PEM data")
175209
}
176-
return x509.ParsePKCS8PrivateKey(blk.Bytes)
210+
key, err := x509.ParsePKCS8PrivateKey(blk.Bytes)
211+
if err != nil {
212+
return nil, err
213+
}
214+
signer, ok := key.(crypto.Signer)
215+
if !ok {
216+
return nil, fmt.Errorf("key is not a signer: %T", key)
217+
}
218+
return signer, nil
177219
}
178220

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

0 commit comments

Comments
 (0)