-
Notifications
You must be signed in to change notification settings - Fork 3
Canton Add additional providers #797
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
stackman27
wants to merge
22
commits into
main
Choose a base branch
from
sish/add-additional-providers
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,229
−115
Open
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
d2dd752
Add additional providers
stackman27 f2eb21d
fix ci
stackman27 86bf168
lint fix
stackman27 2a2621c
fix lint
stackman27 40ddf67
Merge branch 'main' into sish/add-additional-providers
stackman27 d724bf9
add additional providers
stackman27 672809d
Merge branch 'main' into sish/add-additional-providers
stackman27 d3532bf
Merge branch 'main' into sish/add-additional-providers
stackman27 e8eeec1
Merge branch 'main' into sish/add-additional-providers
rodrigombsoares d00c424
Merge branch 'main' into sish/add-additional-providers
rodrigombsoares 310dd80
Refactor Canton OAuth auth to match chainlink-canton patterns.
stackman27 217a55f
Merge branch 'main' into sish/add-additional-providers
stackman27 72a7c65
Align Canton OAuth env vars with CLD secret naming.
stackman27 107e06d
wire up readAs properly
stackman27 b780b0f
rename
stackman27 161b25d
bump mcms
stackman27 28a3a63
Merge branch 'main' into sish/add-additional-providers
stackman27 70f524e
go mod
stackman27 1b99181
fix lint
stackman27 c78e1d0
more lint
stackman27 1c91b20
pablos feedback
stackman27 785d727
fix lint
stackman27 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "chainlink-deployments-framework": minor | ||
| --- | ||
|
|
||
| Extend Canton chain authentication with OAuth2 client credentials (CI) and authorization code (local dev) providers, using RFC 8414 metadata discovery aligned with chainlink-canton authentication packages. |
273 changes: 273 additions & 0 deletions
273
chain/canton/provider/authentication/authorizationcode/authorizationcode.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,273 @@ | ||
| // Package authorizationcode provides OAuth2 authorization code flow authentication for Canton gRPC connections. | ||
| // This flow is intended for local development where a browser-based login is available; it is not suitable for CI. | ||
| package authorizationcode | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/tls" | ||
| "errors" | ||
| "fmt" | ||
| "net" | ||
| "net/http" | ||
| "net/url" | ||
| "os" | ||
| "os/exec" | ||
| "runtime" | ||
| "slices" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "golang.org/x/oauth2" | ||
| "google.golang.org/grpc/credentials" | ||
| "google.golang.org/grpc/credentials/oauth" | ||
|
|
||
| cantonauth "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton/provider/authentication" | ||
| ) | ||
|
|
||
| var _ cantonauth.Provider = Provider{} | ||
|
|
||
| // Provider implements authentication.Provider using the OAuth2 authorization code flow with PKCE (S256). | ||
| type Provider struct { | ||
| tokenSource oauth.TokenSource | ||
| transportCredentials credentials.TransportCredentials | ||
| } | ||
|
|
||
| type authorizationCodeProviderConfig struct { | ||
| scopes []string | ||
| transportCredentials credentials.TransportCredentials | ||
| callbackURL string | ||
| openBrowser bool | ||
| timeout time.Duration | ||
| } | ||
|
|
||
| func defaultAuthorizationCodeProviderConfig() *authorizationCodeProviderConfig { | ||
| return &authorizationCodeProviderConfig{ | ||
| scopes: []string{"openid", "daml_ledger_api"}, | ||
| transportCredentials: credentials.NewTLS(&tls.Config{ | ||
| MinVersion: tls.VersionTLS12, | ||
| }), | ||
| callbackURL: "http://127.0.0.1:8400/callback", | ||
| openBrowser: true, | ||
| } | ||
| } | ||
|
|
||
| // ProviderOption configures the authorization code Provider. | ||
| type ProviderOption func(*authorizationCodeProviderConfig) | ||
|
|
||
| // WithScopes configures the scopes requested from the authorization server. | ||
| func WithScopes(scopes ...string) ProviderOption { | ||
| return func(config *authorizationCodeProviderConfig) { | ||
| config.scopes = scopes | ||
| } | ||
| } | ||
|
|
||
| // WithTransportCredentials configures transport credentials for gRPC connections. | ||
| func WithTransportCredentials(creds credentials.TransportCredentials) ProviderOption { | ||
| return func(config *authorizationCodeProviderConfig) { | ||
| config.transportCredentials = creds | ||
| } | ||
| } | ||
|
|
||
| // WithCallbackURL configures the local redirect URI used by the authorization server. | ||
| func WithCallbackURL(callbackURL string) ProviderOption { | ||
| return func(config *authorizationCodeProviderConfig) { | ||
| config.callbackURL = callbackURL | ||
| } | ||
| } | ||
|
|
||
| // WithOpenBrowser controls whether the default browser is opened automatically. | ||
| func WithOpenBrowser(openBrowser bool) ProviderOption { | ||
| return func(config *authorizationCodeProviderConfig) { | ||
| config.openBrowser = openBrowser | ||
| } | ||
| } | ||
|
|
||
| // WithTimeout configures a timeout for the overall authorization flow. | ||
| func WithTimeout(timeout time.Duration) ProviderOption { | ||
| return func(config *authorizationCodeProviderConfig) { | ||
| config.timeout = timeout | ||
| } | ||
| } | ||
|
|
||
| // NewDiscoveryProvider creates a provider using OAuth2 Authorization Server Metadata discovery (RFC 8414). | ||
| // PKCE with the S256 challenge method is required. | ||
| func NewDiscoveryProvider( | ||
| ctx context.Context, | ||
| authorizationServerURL, clientID string, | ||
| options ...ProviderOption, | ||
| ) (*Provider, error) { | ||
| metadata, err := cantonauth.GetAuthorizationServerMetadata(ctx, authorizationServerURL) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to get authorization server metadata: %w", err) | ||
| } | ||
|
|
||
| if !slices.Contains(metadata.CodeChallengeMethodsSupported, "S256") { | ||
| return nil, errors.New("authorization server does not support S256 PKCE challenges") | ||
| } | ||
|
|
||
| return NewProvider(ctx, metadata.AuthorizationEndpoint, metadata.TokenEndpoint, clientID, options...) | ||
| } | ||
|
|
||
| // NewProvider creates a provider that performs the OAuth2 authorization code flow with PKCE (S256). | ||
| func NewProvider( | ||
| ctx context.Context, | ||
| authURL, tokenURL, clientID string, | ||
| options ...ProviderOption, | ||
| ) (*Provider, error) { | ||
| cfg := defaultAuthorizationCodeProviderConfig() | ||
| for _, option := range options { | ||
| option(cfg) | ||
| } | ||
|
|
||
| if authURL == "" { | ||
| return nil, errors.New("authURL cannot be empty") | ||
| } | ||
| if tokenURL == "" { | ||
| return nil, errors.New("tokenURL cannot be empty") | ||
| } | ||
| if clientID == "" { | ||
| return nil, errors.New("clientID cannot be empty") | ||
| } | ||
|
|
||
| flowCtx := ctx | ||
| if cfg.timeout > 0 { | ||
| var cancel context.CancelFunc | ||
| flowCtx, cancel = context.WithTimeout(ctx, cfg.timeout) | ||
| defer cancel() | ||
| } | ||
|
|
||
| callbackURL, err := url.Parse(cfg.callbackURL) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to parse callback URL: %w", err) | ||
| } | ||
|
stackman27 marked this conversation as resolved.
stackman27 marked this conversation as resolved.
|
||
|
|
||
| oauthCfg := &oauth2.Config{ | ||
| ClientID: clientID, | ||
| RedirectURL: callbackURL.String(), | ||
| Scopes: cfg.scopes, | ||
| Endpoint: oauth2.Endpoint{AuthURL: authURL, TokenURL: tokenURL}, | ||
| } | ||
|
|
||
| state := oauth2.GenerateVerifier() | ||
| verifier := oauth2.GenerateVerifier() | ||
| authCodeURL := oauthCfg.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier)) | ||
|
|
||
| callbackChan := make(chan *oauth2.Token, 1) | ||
| var deliverOnce sync.Once | ||
|
|
||
| serveMux := http.NewServeMux() | ||
| serveMux.HandleFunc(callbackURL.Path, func(w http.ResponseWriter, r *http.Request) { | ||
| q := r.URL.Query() | ||
| code := q.Get("code") | ||
| receivedState := q.Get("state") | ||
|
|
||
| if receivedState != state { | ||
| http.Error(w, "Invalid state parameter", http.StatusBadRequest) | ||
| return | ||
| } | ||
| if code == "" { | ||
| http.Error(w, "No code parameter received", http.StatusBadRequest) | ||
| return | ||
| } | ||
|
|
||
| token, exchangeErr := oauthCfg.Exchange(flowCtx, code, oauth2.VerifierOption(verifier)) | ||
| if exchangeErr != nil { | ||
| fmt.Fprintf(os.Stderr, "authorization code token exchange failed: %v\n", exchangeErr) | ||
| http.Error(w, fmt.Sprintf("Token exchange failed: %v", exchangeErr), http.StatusInternalServerError) | ||
|
|
||
| return | ||
| } | ||
|
|
||
| deliverOnce.Do(func() { | ||
| callbackChan <- token | ||
| }) | ||
|
|
||
| html := `<!DOCTYPE html> | ||
| <html> | ||
| <head><title>Authentication Complete</title></head> | ||
| <body style="font-family: sans-serif; text-align: center; padding: 40px;"> | ||
| <h1>Authentication complete!</h1> | ||
| <p>You can safely close this window.</p> | ||
| </body> | ||
| </html> | ||
| ` | ||
| w.Header().Set("Content-Type", "text/html; charset=utf-8") | ||
| w.WriteHeader(http.StatusOK) | ||
| _, _ = w.Write([]byte(html)) | ||
| }) | ||
|
|
||
| server := http.Server{ | ||
| Addr: callbackURL.Host, | ||
| Handler: serveMux, | ||
| ReadHeaderTimeout: time.Second, | ||
| ReadTimeout: 5 * time.Second, | ||
| WriteTimeout: 5 * time.Second, | ||
| } | ||
|
|
||
| listener, err := new(net.ListenConfig).Listen(flowCtx, "tcp", server.Addr) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("creating listener: %w", err) | ||
| } | ||
|
|
||
| serverErr := make(chan error, 1) | ||
| go func() { | ||
| serverErr <- server.Serve(listener) | ||
| }() | ||
|
|
||
| if cfg.openBrowser { | ||
| fmt.Println("Attempting to open your default browser.") | ||
| fmt.Println("If the browser does not open, visit the following URL:") | ||
| fmt.Println(authCodeURL) | ||
| openBrowser(flowCtx, authCodeURL) | ||
| } else { | ||
| fmt.Println("Visit the following URL:") | ||
| fmt.Println(authCodeURL) | ||
| } | ||
|
|
||
| shutdown := func() { | ||
| shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
| _ = server.Shutdown(shutdownCtx) | ||
| } | ||
|
|
||
| select { | ||
| case err := <-serverErr: | ||
| shutdown() | ||
| return nil, fmt.Errorf("callback server error: %w", err) | ||
| case token := <-callbackChan: | ||
| shutdown() | ||
| refreshCtx := context.WithoutCancel(ctx) | ||
| tokenSource := oauthCfg.TokenSource(refreshCtx, token) | ||
|
|
||
| return &Provider{ | ||
| tokenSource: oauth.TokenSource{TokenSource: tokenSource}, | ||
| transportCredentials: cfg.transportCredentials, | ||
| }, nil | ||
|
stackman27 marked this conversation as resolved.
stackman27 marked this conversation as resolved.
|
||
| case <-flowCtx.Done(): | ||
| shutdown() | ||
| return nil, flowCtx.Err() | ||
| } | ||
| } | ||
|
|
||
| func (p Provider) TokenSource() oauth2.TokenSource { | ||
| return p.tokenSource.TokenSource | ||
| } | ||
|
|
||
| func (p Provider) TransportCredentials() credentials.TransportCredentials { | ||
| return p.transportCredentials | ||
| } | ||
|
|
||
| func (p Provider) PerRPCCredentials() credentials.PerRPCCredentials { | ||
| return p.tokenSource | ||
| } | ||
|
|
||
| func openBrowser(ctx context.Context, targetURL string) { | ||
| switch runtime.GOOS { | ||
| case "darwin": | ||
| _ = exec.CommandContext(ctx, "open", targetURL).Start() | ||
| case "linux": | ||
| _ = exec.CommandContext(ctx, "xdg-open", targetURL).Start() | ||
| case "windows": | ||
| _ = exec.CommandContext(ctx, "rundll32", "url.dll,FileProtocolHandler", targetURL).Start() | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.