Skip to content

Commit ed5dc34

Browse files
authored
feat: adds cognito token source to offchain/jd (#336)
This adds functionality to the `offchain/jd` package to create an oauth2 token source for Cognito. This token is used to authenticate against the Job Distributor API.
1 parent 469a867 commit ed5dc34

File tree

7 files changed

+516
-14
lines changed

7 files changed

+516
-14
lines changed

.changeset/long-chefs-sleep.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
Adds a struct to generate Cognito oauth tokens for Job Distributor

.mockery.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ packages:
2424
filename: "mock_{{.InterfaceName | snakecase}}.go"
2525
interfaces:
2626
Client:
27+
github.com/smartcontractkit/chainlink-deployments-framework/offchain/jd:
28+
config:
29+
all: false
30+
pkgname: "mocks"
31+
dir: "offchain/jd/internal/mocks"
32+
filename: "mock_{{.InterfaceName | snakecase}}.go"
33+
interfaces:
34+
CognitoClient:
2735
github.com/smartcontractkit/chainlink-protos/job-distributor/v1/job:
2836
config:
2937
all: false
@@ -48,4 +56,3 @@ packages:
4856
filename: "mock_{{.InterfaceName | snakecase}}.go"
4957
interfaces:
5058
NodeServiceClient:
51-

go.mod

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ require (
99
github.com/aptos-labs/aptos-go-sdk v1.6.3-0.20250331001805-0680b714db6d
1010
github.com/avast/retry-go/v4 v4.6.1
1111
github.com/aws/aws-sdk-go v1.55.7
12+
github.com/aws/aws-sdk-go-v2 v1.38.1
13+
github.com/aws/aws-sdk-go-v2/config v1.28.0
14+
github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.57.1
1215
github.com/block-vision/sui-go-sdk v1.0.6
1316
github.com/cosmos/go-bip39 v1.0.0
1417
github.com/ethereum/go-ethereum v1.15.7
@@ -54,20 +57,18 @@ require (
5457
github.com/XSAM/otelsql v0.37.0 // indirect
5558
github.com/avast/retry-go v3.0.0+incompatible // indirect
5659
github.com/awalterschulze/gographviz v2.0.3+incompatible // indirect
57-
github.com/aws/aws-sdk-go-v2 v1.32.2 // indirect
58-
github.com/aws/aws-sdk-go-v2/config v1.28.0 // indirect
5960
github.com/aws/aws-sdk-go-v2/credentials v1.17.41 // indirect
6061
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 // indirect
61-
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect
62-
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect
62+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 // indirect
63+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 // indirect
6364
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
6465
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect
6566
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect
6667
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2 // indirect
6768
github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 // indirect
6869
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 // indirect
6970
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 // indirect
70-
github.com/aws/smithy-go v1.22.0 // indirect
71+
github.com/aws/smithy-go v1.22.5 // indirect
7172
github.com/bahlo/generic-list-go v0.2.0 // indirect
7273
github.com/benbjohnson/clock v1.3.5 // indirect
7374
github.com/beorn7/perks v1.0.1 // indirect

go.sum

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,22 @@ github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2
3939
github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs=
4040
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
4141
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
42-
github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI=
43-
github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
42+
github.com/aws/aws-sdk-go-v2 v1.38.1 h1:j7sc33amE74Rz0M/PoCpsZQ6OunLqys/m5antM0J+Z8=
43+
github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
4444
github.com/aws/aws-sdk-go-v2/config v1.28.0 h1:FosVYWcqEtWNxHn8gB/Vs6jOlNwSoyOCA/g/sxyySOQ=
4545
github.com/aws/aws-sdk-go-v2/config v1.28.0/go.mod h1:pYhbtvg1siOOg8h5an77rXle9tVG8T+BWLWAo7cOukc=
4646
github.com/aws/aws-sdk-go-v2/credentials v1.17.41 h1:7gXo+Axmp+R4Z+AK8YFQO0ZV3L0gizGINCOWxSLY9W8=
4747
github.com/aws/aws-sdk-go-v2/credentials v1.17.41/go.mod h1:u4Eb8d3394YLubphT4jLEwN1rLNq2wFOlT6OuxFwPzU=
4848
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 h1:TMH3f/SCAWdNtXXVPPu5D6wrr4G5hI1rAxbcocKfC7Q=
4949
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17/go.mod h1:1ZRXLdTpzdJb9fwTMXiLipENRxkGMTn1sfKexGllQCw=
50-
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk=
51-
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y=
52-
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s=
53-
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60=
50+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 h1:IdCLsiiIj5YJ3AFevsewURCPV+YWUlOW8JiPhoAy8vg=
51+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao=
52+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 h1:j7vjtr1YIssWQOMeOWRbh3z8g2oY/xPjnZH2gLY4sGw=
53+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA=
5454
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
5555
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
56+
github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.57.1 h1:gKFnV8HEJomx4XFOVBXRUA5hphkhpnUjqJsYPCc9K8Q=
57+
github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.57.1/go.mod h1:+UxryRSMGMtqsvxdnws+VpNyFYWRkw4ZlM+5AC160XA=
5658
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g=
5759
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ=
5860
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA=
@@ -65,8 +67,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 h1:AhmO1fHINP9vFYUE0LHzCWg/
6567
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2/go.mod h1:o8aQygT2+MVP0NaV6kbdE1YnnIM8RRVQzoeUH45GOdI=
6668
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo=
6769
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo=
68-
github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM=
69-
github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
70+
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
71+
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
7072
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
7173
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
7274
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0=

offchain/jd/cognito.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package jd
2+
3+
import (
4+
"context"
5+
"crypto/hmac"
6+
"crypto/sha256"
7+
"encoding/base64"
8+
9+
"github.com/aws/aws-sdk-go-v2/aws"
10+
"github.com/aws/aws-sdk-go-v2/config"
11+
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
12+
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider/types"
13+
"golang.org/x/oauth2"
14+
)
15+
16+
// CognitoClient defines the interface for Cognito Identity Provider operations.
17+
// This interface allows for mocking the AWS Cognito client in unit tests.
18+
type CognitoClient interface {
19+
InitiateAuth(
20+
ctx context.Context,
21+
params *cognitoidentityprovider.InitiateAuthInput,
22+
optFns ...func(*cognitoidentityprovider.Options),
23+
) (*cognitoidentityprovider.InitiateAuthOutput, error)
24+
}
25+
26+
// CognitoTokenSource provides a oath2 token that is used to authenticate with the Job Distributor.
27+
type CognitoTokenSource struct {
28+
// The Cognito authentication information
29+
auth CognitoAuth
30+
31+
// The cached authentication result from Cognito
32+
authResult *types.AuthenticationResultType
33+
34+
// The Cognito client interface for making API calls
35+
client CognitoClient
36+
}
37+
38+
// CognitoAuth contains the Cognito authentication information required to generate a token from
39+
// Cognito.
40+
type CognitoAuth struct {
41+
AWSRegion string
42+
CognitoAppClientID string
43+
CognitoAppClientSecret string
44+
Username string
45+
Password string
46+
}
47+
48+
// NewCognitoTokenSource creates a new CognitoTokenSource with the given CognitoAuth configuration.
49+
// If client is nil, a real AWS Cognito client will be created when Authenticate is called.
50+
func NewCognitoTokenSource(auth CognitoAuth) *CognitoTokenSource {
51+
return &CognitoTokenSource{
52+
auth: auth,
53+
}
54+
}
55+
56+
// Authenticate performs user authentication against AWS Cognito using the USER_PASSWORD_AUTH flow.
57+
//
58+
// Authentication results are cached in the authResult field and used by the Token() method
59+
// to provide OAuth2 access tokens without re-authenticating.
60+
func (c *CognitoTokenSource) Authenticate(ctx context.Context) error {
61+
// Create client if not already set (for production use)
62+
if c.client == nil {
63+
sdkConfig, err := config.LoadDefaultConfig(ctx,
64+
config.WithRegion(c.auth.AWSRegion),
65+
)
66+
if err != nil {
67+
return err
68+
}
69+
c.client = cognitoidentityprovider.NewFromConfig(sdkConfig)
70+
}
71+
72+
// Authenticate the user
73+
input := &cognitoidentityprovider.InitiateAuthInput{
74+
AuthFlow: types.AuthFlowTypeUserPasswordAuth,
75+
ClientId: aws.String(c.auth.CognitoAppClientID),
76+
AuthParameters: map[string]string{
77+
"USERNAME": c.auth.Username,
78+
"PASSWORD": c.auth.Password,
79+
"SECRET_HASH": c.secretHash(),
80+
},
81+
}
82+
83+
output, err := c.client.InitiateAuth(ctx, input)
84+
if err != nil {
85+
return err
86+
}
87+
88+
c.authResult = output.AuthenticationResult
89+
90+
return nil
91+
}
92+
93+
// Token retrieves an OAuth2 access token for authenticating with the Job Distributor service.
94+
//
95+
// This method implements a lazy loading pattern:
96+
// 1. If an authentication result is already cached, it returns the cached access token
97+
// 2. If no cached result exists, it automatically authenticates with AWS Cognito first
98+
// 3. Returns the access token wrapped in an oauth2.Token struct
99+
//
100+
// The method implements the oauth2.TokenSource interface, making it compatible with
101+
// standard OAuth2 client libraries and HTTP clients that support token sources.
102+
//
103+
// Note: This method uses context.Background() for authentication if no cached token exists.
104+
// For more control over authentication context and timeout behavior, consider calling
105+
// Authenticate() explicitly before calling Token().
106+
//
107+
// Returns an OAuth2 token containing the Cognito access token
108+
func (c *CognitoTokenSource) Token() (*oauth2.Token, error) {
109+
if c.authResult == nil {
110+
if err := c.Authenticate(context.Background()); err != nil {
111+
return nil, err
112+
}
113+
}
114+
115+
return &oauth2.Token{
116+
AccessToken: aws.ToString(c.authResult.AccessToken),
117+
}, nil
118+
}
119+
120+
// secretHash computes the AWS Cognito secret hash required for authentication with app clients that have a client secret.
121+
//
122+
// The secret hash is calculated using HMAC-SHA256 with the following formula:
123+
//
124+
// HMAC-SHA256(ClientSecret, Username + ClientId)
125+
//
126+
// This method:
127+
// 1. Creates an HMAC-SHA256 hash using the Cognito app client secret as the key
128+
// 2. Constructs the message by concatenating the username and client ID
129+
// 3. Computes the HMAC hash of the message
130+
// 4. Returns the hash encoded as a base64 string
131+
//
132+
// The secret hash is required by AWS Cognito when the app client is configured with a client secret.
133+
// It provides an additional layer of security by ensuring that only clients with the correct secret
134+
// can authenticate users.
135+
//
136+
// Returns the computed secret hash as a base64-encoded string.
137+
func (c *CognitoTokenSource) secretHash() string {
138+
hmac := hmac.New(sha256.New, []byte(c.auth.CognitoAppClientSecret))
139+
message := []byte(c.auth.Username + c.auth.CognitoAppClientID)
140+
hmac.Write(message)
141+
dataHmac := hmac.Sum(nil)
142+
143+
return base64.StdEncoding.EncodeToString(dataHmac)
144+
}

0 commit comments

Comments
 (0)