Skip to content

Canton Add additional providers#797

Open
stackman27 wants to merge 21 commits into
mainfrom
sish/add-additional-providers
Open

Canton Add additional providers#797
stackman27 wants to merge 21 commits into
mainfrom
sish/add-additional-providers

Conversation

@stackman27
Copy link
Copy Markdown
Contributor

@stackman27 stackman27 commented Feb 26, 2026

Canton chain support previously only accepted a pre-obtained static JWT via ONCHAIN_CANTON_JWT_TOKEN. That works for manual use but doesn't support automated CI pipelines or interactive local development against Okta.

This PR adds two OAuth2 authentication strategies for Canton, aligned with the chainlink-canton authentication packages:

client_credentials — for CI/CD. Fetches tokens machine-to-machine using client_id, client_secret, and the Okta authorization server URL. Set ONCHAIN_CANTON_AUTH_STRATEGY=client_credentials along with ONCHAIN_CANTON_OKTA_AUTHORIZER, ONCHAIN_CANTON_OKTA_CLIENT_ID, and ONCHAIN_CANTON_OKTA_CLIENT_SECRET.

authorization_code — for local development only. Opens a browser for interactive OAuth with PKCE. Not suitable for CI. Set ONCHAIN_CANTON_AUTH_STRATEGY=authorization_code along with ONCHAIN_CANTON_OKTA_AUTHORIZER and ONCHAIN_CANTON_OKTA_CLIENT_ID. Default callback is http://127.0.0.1:8400/callback (configurable via WithCallbackURL).

static — unchanged. Continue using ONCHAIN_CANTON_JWT_TOKEN for a pre-obtained JWT.

Both OAuth flows use RFC 8414 authorization server metadata discovery. Providers are in chain/canton/provider/authentication/clientcredentials and authorizationcode, with unit tests. The CLD chain loader selects the provider based on auth_strategy.

@stackman27 stackman27 requested a review from a team as a code owner February 26, 2026 20:14
Copilot AI review requested due to automatic review settings February 26, 2026 20:14
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 26, 2026

🦋 Changeset detected

Latest commit: 1c91b20

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
chainlink-deployments-framework Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown

👋 stackman27, thanks for creating this pull request!

To help reviewers, please consider creating future PRs as drafts first. This allows you to self-review and make any final changes before notifying the team.

Once you're ready, you can mark it as "Ready for review" to request feedback. Thanks!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds OAuth2 authentication support for Canton chains, extending the existing static JWT token authentication with two new OAuth2 flows: client credentials (for CI environments) and authorization code with PKCE (for local development).

Changes:

  • Introduced three authentication types for Canton: static (existing JWT), client_credentials (OAuth2 for CI), and authorization_code (OAuth2 with browser flow for local development)
  • Added new configuration fields to CantonConfig for OAuth2 credentials (AuthURL, ClientID, ClientSecret) with corresponding environment variable mappings
  • Implemented OIDCProvider with support for both OAuth2 flows, including automatic token refresh capabilities

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 15 comments.

File Description
engine/cld/config/env/config.go Added auth type constants and new OAuth2 configuration fields to CantonConfig with environment variable mappings
engine/cld/chains/chains.go Refactored Canton chain loader to support multiple auth types with provider factory pattern and enhanced configuration validation
chain/canton/provider/authentication/oauth.go New file implementing OIDCProvider with client credentials and authorization code flows, including local callback server for browser-based auth

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread engine/cld/chains/chains.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread engine/cld/chains/chains.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread engine/cld/config/env/config.go
Copilot AI review requested due to automatic review settings February 26, 2026 21:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 16 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread engine/cld/config/env/config.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread engine/cld/chains/chains.go Outdated
Comment thread engine/cld/chains/chains.go Outdated
Comment thread engine/cld/chains/chains.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Copilot AI review requested due to automatic review settings February 27, 2026 01:05
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread engine/cld/chains/chains.go Outdated
Comment on lines +780 to +825
// cantonAuthConfigured returns true if Canton auth is configured for at least one scheme (static, client_credentials, or authorization_code).
func cantonAuthConfigured(c cfgenv.CantonConfig) bool {
switch c.AuthType {
case cfgenv.CantonAuthTypeClientCredentials:
return c.AuthURL != "" && c.ClientID != "" && c.ClientSecret != ""
case cfgenv.CantonAuthTypeAuthorizationCode:
return c.AuthURL != "" && c.ClientID != ""
default:
// static or empty (backward compat: jwt_token alone enables Canton)
return c.JWTToken != ""
}
}

// cantonAuthProvider builds a Canton auth Provider from config. Caller must ensure cantonAuthConfigured(cfg.Canton) is true.
func (l *chainLoaderCanton) cantonAuthProvider(ctx context.Context, selector uint64) (cantonauth.Provider, error) {
c := l.cfg.Canton
switch c.AuthType {
case cfgenv.CantonAuthTypeClientCredentials:
if c.AuthURL == "" || c.ClientID == "" || c.ClientSecret == "" {
return nil, fmt.Errorf("canton network %d: client_credentials requires auth_url, client_id, and client_secret", selector)
}
oidc, err := cantonauth.NewClientCredentialsProvider(ctx, c.AuthURL, c.ClientID, c.ClientSecret)
if err != nil {
return nil, fmt.Errorf("canton network %d: client_credentials auth: %w", selector, err)
}

return oidc, nil
case cfgenv.CantonAuthTypeAuthorizationCode:
if c.AuthURL == "" || c.ClientID == "" {
return nil, fmt.Errorf("canton network %d: authorization_code requires auth_url and client_id", selector)
}
oidc, err := cantonauth.NewAuthorizationCodeProvider(ctx, c.AuthURL, c.ClientID)
if err != nil {
return nil, fmt.Errorf("canton network %d: authorization_code auth: %w", selector, err)
}

return oidc, nil
default:
// static or empty
if c.JWTToken == "" {
return nil, fmt.Errorf("canton network %d: JWT token is required for static auth", selector)
}

return cantonauth.NewStaticProvider(c.JWTToken), nil
}
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new cantonAuthProvider and cantonAuthConfigured helper functions lack test coverage. The existing Test_chainLoaderCanton_Load test only covers static JWT authentication and needs to be extended with test cases for the new client_credentials and authorization_code auth types to verify correct provider selection, validation, and error handling for each authentication scheme.

Copilot uses AI. Check for mistakes.
Comment thread engine/cld/chains/chains.go Outdated
Comment on lines +797 to +810
case cfgenv.CantonAuthTypeClientCredentials:
if c.AuthURL == "" || c.ClientID == "" || c.ClientSecret == "" {
return nil, fmt.Errorf("canton network %d: client_credentials requires auth_url, client_id, and client_secret", selector)
}
oidc, err := cantonauth.NewClientCredentialsProvider(ctx, c.AuthURL, c.ClientID, c.ClientSecret)
if err != nil {
return nil, fmt.Errorf("canton network %d: client_credentials auth: %w", selector, err)
}

return oidc, nil
case cfgenv.CantonAuthTypeAuthorizationCode:
if c.AuthURL == "" || c.ClientID == "" {
return nil, fmt.Errorf("canton network %d: authorization_code requires auth_url and client_id", selector)
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation in cantonAuthProvider duplicates the checks already performed in cantonAuthConfigured. This creates a maintainability issue where validation logic must be kept in sync across two functions. Consider refactoring to reuse cantonAuthConfigured checks or returning more specific errors from cantonAuthConfigured to avoid redundant validation.

Copilot uses AI. Check for mistakes.
Comment on lines +156 to +160
func generateState() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err)
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The panic in generateState on cryptographic random number generation failure could crash the application. While crypto/rand.Read failures are extremely rare in practice, consider returning an error instead of panicking to allow graceful error handling and recovery at the caller level.

Copilot uses AI. Check for mistakes.
Comment on lines +121 to +134
select {
case err := <-serverErr:
_ = server.Shutdown(ctx)

return nil, fmt.Errorf("callback server error: %w", err)
case token := <-callbackChan:
tokenSource := oauthCfg.TokenSource(ctx, token)
_ = server.Shutdown(ctx)

return &OIDCProvider{
tokenSource: tokenSource,
}, nil
case <-ctx.Done():
_ = server.Shutdown(ctx)
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTTP server shutdown is called with the already-cancelled context in the ctx.Done() case. This means Shutdown will return immediately without waiting for graceful shutdown. Consider using a separate timeout context for shutdown (e.g., context.WithTimeout(context.Background(), 5*time.Second)) to allow ongoing requests to complete before forcing server termination.

Suggested change
select {
case err := <-serverErr:
_ = server.Shutdown(ctx)
return nil, fmt.Errorf("callback server error: %w", err)
case token := <-callbackChan:
tokenSource := oauthCfg.TokenSource(ctx, token)
_ = server.Shutdown(ctx)
return &OIDCProvider{
tokenSource: tokenSource,
}, nil
case <-ctx.Done():
_ = server.Shutdown(ctx)
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
select {
case err := <-serverErr:
_ = server.Shutdown(shutdownCtx)
return nil, fmt.Errorf("callback server error: %w", err)
case token := <-callbackChan:
tokenSource := oauthCfg.TokenSource(ctx, token)
_ = server.Shutdown(shutdownCtx)
return &OIDCProvider{
tokenSource: tokenSource,
}, nil
case <-ctx.Done():
_ = server.Shutdown(shutdownCtx)

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 6, 2026 01:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment on lines +22 to +26

// OIDCProvider implements Provider using OAuth2/OIDC token flows (client credentials or authorization code).
type OIDCProvider struct {
tokenSource oauth2.TokenSource
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces OIDCProvider but there are no unit tests covering it (unlike the existing StaticProvider tests). Add focused tests for TransportCredentials/PerRPCCredentials behavior and token acquisition (e.g., via an httptest token endpoint) to prevent regressions.

Copilot uses AI. Check for mistakes.
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread engine/cld/config/env/config_test.go Outdated
Comment on lines +48 to +52
AuthType: "",
JWTToken: "",
AuthURL: "",
ClientID: "",
ClientSecret: "",
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CantonConfig is always expected to be empty in these fixtures, so the tests won’t catch mistakes in the newly added Canton env var bindings (ONCHAIN_CANTON_AUTH_TYPE/AUTH_URL/CLIENT_ID/CLIENT_SECRET). Add a test case that sets these env vars (for oauth and/or static) and asserts they unmarshal into CantonConfig correctly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Much like other Fields, set some data in here to assert that the values are unmarshalled correctly

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. Added Canton env vars (ONCHAIN_CANTON_AUTH_STRATEGY, ONCHAIN_CANTON_OKTA_*, ONCHAIN_CANTON_JWT_TOKEN) to the shared envVars/envCfg fixtures and assert them via Test_Load, Test_LoadEnv, and Test_LoadEnv_Legacy

Comment thread .changeset/canton-chain-support.md Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
func NewAuthorizationCodeProvider(ctx context.Context, authURL, clientID string) (*OIDCProvider, error) {
verifier := oauth2.GenerateVerifier()

port := 8400
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the port be hardcoded here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, kept 8400 as the default (Okta redirect URIs must match), but made it configurable via WithCallbackURL and bound to 127.0.0.1 only.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do you expect to use this authentication?

I see you there is code to open a browser to confirm, but this won't work in CI

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

authorization_code is for local dev only (browser OAuth via authorizationcode package). CI uses client_credentials (clientcredentials package) or a pre-set static JWT, documented in package comments and CantonAuthStrategy constants

@@ -0,0 +1,175 @@
package authentication
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please write tests for this package

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, split into clientcredentials and authorizationcode subpackages, each with unit tests covering validation, metadata discovery, and token acquisition.

@jkongie
Copy link
Copy Markdown
Collaborator

jkongie commented Mar 6, 2026

Please update the description to explain why this change is being introduce.

This helps the reviewer with context on what is being reviewed

Comment thread engine/cld/config/env/config.go Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread engine/cld/config/env/config_test.go Outdated
Comment thread engine/cld/chains/chains.go Outdated
Comment thread engine/cld/chains/chains.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread chain/canton/provider/authentication/oauth.go Outdated
Comment thread engine/cld/chains/chains.go Outdated
@cl-sonarqube-production
Copy link
Copy Markdown

Quality Gate failed Quality Gate failed

Failed conditions
12.3% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube

@stackman27 stackman27 marked this pull request as draft May 7, 2026 08:38
stackman27 and others added 3 commits May 26, 2026 22:58
Split oauth.go into clientcredentials and authorizationcode packages with
RFC 8414 discovery, rename AuthType to AuthStrategy, and add test coverage.
Resolve go.mod conflict by taking main's dependency versions.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use CANTON_OKTA_AUTHORIZER, CANTON_OKTA_CLIENT_ID, and
CANTON_OKTA_CLIENT_SECRET so CLDF matches the keys registered in CLD CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread engine/cld/config/env/config.go Outdated
@stackman27 stackman27 marked this pull request as ready for review June 5, 2026 20:02
Copilot AI review requested due to automatic review settings June 5, 2026 20:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 5 comments.

Comment on lines +791 to +799
switch c.AuthStrategy {
case cfgenv.CantonAuthStrategyClientCredentials:
return c.AuthURL != "" && c.ClientID != "" && c.ClientSecret != ""
case cfgenv.CantonAuthStrategyAuthorizationCode:
return c.AuthURL != "" && c.ClientID != ""
default:
// static or empty (backward compat: jwt_token alone enables Canton)
return c.JWTToken != ""
}
Comment thread engine/cld/chains/chains.go
Comment on lines +224 to 228
if cantonAuthConfigured(cfg.Canton) {
loaders[chainsel.FamilyCanton] = newChainLoaderCanton(networks, cfg)
} else {
lggr.Info("Skipping Canton chains, no JWT token found in secrets")
lggr.Info("Skipping Canton chains, no Canton auth configured (set auth_strategy and jwt_token, or auth_url+client_id for OAuth)")
}
Copilot AI review requested due to automatic review settings June 5, 2026 20:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 6 comments.

Comment thread engine/cld/chains/chains.go
Comment thread engine/cld/chains/chains.go
Comment thread engine/cld/chains/chains.go
Comment thread chain/canton/provider/authentication/metadata.go
@cl-sonarqube-production
Copy link
Copy Markdown

Quality Gate failed Quality Gate failed

Failed conditions
77.8% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube

Copy link
Copy Markdown
Contributor

@ecPablo ecPablo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @stackman27, left some comments. I'll defer the final stamp to @jkongie and @graham-chainlink as they have more context on cldf to provide feedback on the integration


if l.cfg.Canton.JWTToken == "" {
return nil, fmt.Errorf("canton network %d: JWT token is required", selector)
authProvider, err := l.cantonAuthProvider(ctx, selector, md.InsecureTransport)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering if this md.InsecureTransport should be configurable? I'm guessing in prod environments we don't want this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already configurable per-network in metadata; defaults off. Only used for local plaintext gRPC with static JWT.

Comment thread chain/canton/provider/authentication/authorizationcode/authorizationcode.go Outdated
Comment thread chain/canton/provider/authentication/authorizationcode/authorizationcode.go Outdated
@graham-chainlink graham-chainlink requested a review from jkongie June 6, 2026 02:17
Copilot AI review requested due to automatic review settings June 7, 2026 06:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

Comment on lines +236 to +243
case token := <-callbackChan:
shutdown()
tokenSource := oauthCfg.TokenSource(flowCtx, token)

return &Provider{
tokenSource: oauth.TokenSource{TokenSource: tokenSource},
transportCredentials: cfg.transportCredentials,
}, nil
Comment on lines +805 to +829
switch c.AuthStrategy {
case cfgenv.CantonAuthStrategyClientCredentials:
provider, err := cantonclientcreds.NewDiscoveryProvider(ctx, c.AuthURL, c.ClientID, c.ClientSecret)
if err != nil {
return nil, fmt.Errorf("canton network %d: client_credentials auth: %w", selector, err)
}

return provider, nil
case cfgenv.CantonAuthStrategyAuthorizationCode:
provider, err := cantonauthcode.NewDiscoveryProvider(ctx, c.AuthURL, c.ClientID)
if err != nil {
return nil, fmt.Errorf("canton network %d: authorization_code auth: %w", selector, err)
}

return provider, nil
default:
if c.JWTToken == "" {
return nil, fmt.Errorf("canton network %d: JWT token is required for static auth", selector)
}
if insecureTransport {
return cantonauth.NewInsecureStaticProvider(c.JWTToken), nil
}

return cantonauth.NewStaticProvider(c.JWTToken), nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants