Skip to content

Conversation

CL-Andrew
Copy link
Collaborator

@CL-Andrew CL-Andrew commented Sep 12, 2024

This PR introduces a new authentication driver for the core node session auth flow for the Operator UI. This extends the options to local auth, LDAP server authentication, and now OIDC auth. This means that SSO sign on and RBAC roles can be managed from a provider/identity server that manages and maps user group membership.

The only required config to support this new authentication method looks as follows:

# config.toml
[WebServer.OIDC]
ClientID = "{CLIENT_ID}"
ProviderURL = 'https://dev-14912014.okta.com/oauth2/default'
RedirectURL = 'http://localhost:3000/signin' 
ClaimKey = "groups"
AdminClaim = 'NodeAdmins' 
EditClaim = 'NodeEditors' 
RunClaim = 'NodeRunners' 
ReadClaim = 'NodeReadOnly' 
SessionTimeout = '15m0s'
UserApiTokenEnabled = false 
UserAPITokenDuration = '240h0m0s' 
...

# secrets.toml
[WebServer.OIDC]
clientSecret = "{{OIDC_CLIENT_SECRET}}"

See Front-end changes
smartcontractkit/operator-ui#104

@CL-Andrew CL-Andrew self-assigned this Sep 12, 2024
@CL-Andrew CL-Andrew requested a review from a team as a code owner September 12, 2024 07:16
@CL-Andrew CL-Andrew requested review from vyzaldysanchez and removed request for a team September 12, 2024 07:16
@harry-anderson harry-anderson requested a review from a team as a code owner February 12, 2025 22:01
@cl-sonarqube-production
Copy link

Quality Gate failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube

Catch issues before they fail your Quality Gate with our IDE extension SonarQube IDE SonarQube IDE

Copy link
Contributor

AER Report: CI Core

aer_workflow , commit , Clean Go Tidy & Generate , Detect Changes , Scheduled Run Frequency , GolangCI Lint (.) , Core Tests (go_core_tests) , test-scripts , Core Tests (go_core_tests_integration) , Core Tests (go_core_ccip_deployment_tests) , Core Tests (go_core_fuzz) , Core Tests (go_core_race_tests) , lint , SonarQube Scan

1. Duplicate migration version found:go_core_race_tests

Source of Error:
Setup DB	2025-02-25T05:09:38.7631606Z FATAL	Failed to reset database:migrateDB failed: failed to create goose provider: found duplicate migration version 253:
Setup DB	2025-02-25T05:09:38.7632571Z 	existing:0253_add_oidc_auth_tables.sql
Setup DB	2025-02-25T05:09:38.7633071Z 	current:0253_add_spec_type_to_workflow_spec.sql
**Why**: The error occurs because there are two migration files with the same version number (253), which causes a conflict during the database migration process.

Suggested fix: Rename one of the migration files to have a unique version number.

2. Duplicate migration version found:go_core_tests_integration

Source of Error:
Setup DB	2025-02-25T05:09:38.4665245Z FATAL	Failed to reset database:migrateDB failed: failed to create goose provider: found duplicate migration version 253:
Setup DB	2025-02-25T05:09:38.4666868Z 	existing:0253_add_oidc_auth_tables.sql
Setup DB	2025-02-25T05:09:38.4667337Z 	current:0253_add_spec_type_to_workflow_spec.sql
**Why**: The error occurs because there are two migration files with the same version number (253), which causes a conflict during the database migration process.

Suggested fix: Rename one of the migration files to have a unique version number.

3. Missing go.sum entry: test-scripts

Source of Error:
Run Tests	2025-02-25T05:07:22.8641335Z ##[error]../sessions/oidcauth/oidc.go:28:2: missing go.sum entry for module providing package github.com/coreos/go-oidc/v3/oidc (imported by github.com/smartcontractkit/chainlink/v2/core/sessions/oidcauth); to add:
Run Tests	2025-02-25T05:07:22.8649486Z 	go get github.com/smartcontractkit/chainlink/v2/core/sessions/[email protected]
**Why**: The error occurs because the `go.sum` file is missing an entry for the `github.com/coreos/go-oidc/v3/oidc` package.

Suggested fix: Run go get github.com/smartcontractkit/chainlink/v2/core/sessions/[email protected] to update the go.sum file.

4. Duplicate migration version found:go_core_ccip_deployment_tests

Source of Error:
Setup DB	2025-02-25T05:09:53.7201903Z FATAL	Failed to reset database:migrateDB failed: failed to create goose provider: found duplicate migration version 253:
Setup DB	2025-02-25T05:09:53.7202612Z 	existing:0253_add_oidc_auth_tables.sql
Setup DB	2025-02-25T05:09:53.7202963Z 	current:0253_add_spec_type_to_workflow_spec.sql
**Why**: The error occurs because there are two migration files with the same version number (253), which causes a conflict during the database migration process.

Suggested fix: Rename one of the migration files to have a unique version number.

5. GolangCI Lint errors: GolangCI Lint (.)

Source of Error:
Golang Lint (.)	2025-02-25T05:10:04.1623275Z ##[error]core/sessions/oidcauth/reaper.go:26:30: cannot use &sessionReaper{…} (value of type *sessionReaper) as "github.com/smartcontractkit/chainlink-common/pkg/utils".Worker value in argument to utils.NewSleeperTask: *sessionReaper does not implement "github.com/smartcontractkit/chainlink-common/pkg/utils".Worker (wrong type for method Work)
Golang Lint (.)	2025-02-25T05:10:04.1625444Z 		have Work("context".Context)
Golang Lint (.)	2025-02-25T05:10:04.1625859Z 		want Work() (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1628323Z ##[error]core/internal/cltest/cltest.go:266:38: app.KeyStore undefined (type *TestApplication has no field or method KeyStore) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1629349Z 		k, _ := MustInsertRandomKey(t, app.KeyStore.Eth(), chainID)
Golang Lint (.)	2025-02-25T05:10:04.1630075Z 		 ^
Golang Lint (.)	2025-02-25T05:10:04.1631643Z ##[error]core/internal/cltest/cltest.go:269:34: app.KeyStore undefined (type *TestApplication has no field or method KeyStore) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1632625Z 		id, ks := chainID.ToInt(), app.KeyStore.Eth()
Golang Lint (.)	2025-02-25T05:10:04.1633272Z 		 ^
Golang Lint (.)	2025-02-25T05:10:04.1634711Z ##[error]core/internal/cltest/cltest.go:288:27: app.GetKeyStore undefined (type *TestApplication has no field or method GetKeyStore) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1635782Z 			require.NoError(t, app.GetKeyStore().P2P().Add(ctx, v))
Golang Lint (.)	2025-02-25T05:10:04.1636394Z 			 ^
Golang Lint (.)	2025-02-25T05:10:04.1637771Z ##[error]core/internal/cltest/cltest.go:290:27: app.GetKeyStore undefined (type *TestApplication has no field or method GetKeyStore) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1638834Z 			require.NoError(t, app.GetKeyStore().CSA().Add(ctx, v))
Golang Lint (.)	2025-02-25T05:10:04.1639430Z 			 ^
Golang Lint (.)	2025-02-25T05:10:04.1640941Z ##[error]core/internal/cltest/cltest.go:292:27: app.GetKeyStore undefined (type *TestApplication has no field or method GetKeyStore) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1642004Z 			require.NoError(t, app.GetKeyStore().OCR2().Add(ctx, v))
Golang Lint (.)	2025-02-25T05:10:04.1642609Z 			 ^
Golang Lint (.)	2025-02-25T05:10:04.1643910Z ##[error]core/internal/cltest/cltest.go:659:12: ta.GetDB undefined (type *TestApplication has no field or method GetDB) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1645334Z 	err := ta.GetDB().GetContext(ctx, &id, `INSERT INTO sessions (id, email, last_used, created_at) VALUES ($1, $2, $3, NOW()) RETURNING id`, session.ID, email, session.LastUsed)
Golang Lint (.)	2025-02-25T05:10:04.1646781Z 	 ^
Golang Lint (.)	2025-02-25T05:10:04.1648160Z ##[error]core/internal/cltest/cltest.go:666:27: ta.KeyStore undefined (type *TestApplication has no field or method KeyStore) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1649150Z 	require.NoError(ta.t, ta.KeyStore.Unlock(ctx, Password))
Golang Lint (.)	2025-02-25T05:10:04.1649758Z 	 ^
Golang Lint (.)	2025-02-25T05:10:04.1651214Z ##[error]core/internal/cltest/cltest.go:667:15: ta.KeyStore undefined (type *TestApplication has no field or method KeyStore) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1652324Z 	_, err := ta.KeyStore.Eth().Import(ctx, []byte(content), Password, &FixtureChainID)
Golang Lint (.)	2025-02-25T05:10:04.1653238Z 	 ^
Golang Lint (.)	2025-02-25T05:10:04.1654795Z ##[error]core/internal/cltest/cltest.go:695:11: ta.BasicAdminUsersORM undefined (type *TestApplication has no field or method BasicAdminUsersORM) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1655849Z 	err = ta.BasicAdminUsersORM().CreateUser(ctx, &u)
Golang Lint (.)	2025-02-25T05:10:04.1656305Z 	 ^
Golang Lint (.)	2025-02-25T05:10:04.1657606Z ##[error]core/internal/cltest/cltest.go:717:38: ta.GetConfig undefined (type *TestApplication has no field or method GetConfig) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1658548Z 		Config: ta.GetConfig(),
Golang Lint (.)	2025-02-25T05:10:04.1659210Z 		 ^
Golang Lint (.)	2025-02-25T05:10:04.1660588Z ##[error]core/internal/cltest/cltest.go:736:38: ta.GetConfig undefined (type *TestApplication has no field or method GetConfig) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1661703Z 		Config: ta.GetConfig(),
Golang Lint (.)	2025-02-25T05:10:04.1662373Z 		 ^
Golang Lint (.)	2025-02-25T05:10:04.1663794Z ##[error]core/internal/cltest/cltest.go:1328:95: app.GetConfig undefined (type *TestApplication has no field or method GetConfig) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1665201Z 	ethClient.On("ConfiguredChainID", mock.Anything).Return(evmtest.MustGetDefaultChainID(t, app.GetConfig().EVMConfigs()), nil)
Golang Lint (.)	2025-02-25T05:10:04.1667472Z 	 ^
Golang Lint (.)	2025-02-25T05:10:04.1669302Z ##[error]core/capabilities/integration_tests/framework/don.go:291:18: node.Stop undefined (type *capabilityNode has no field or method Stop) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1670294Z 		if err := node.Stop(); err != nil {
Golang Lint (.)	2025-02-25T05:10:04.1670878Z 		 ^
Golang Lint (.)	2025-02-25T05:10:04.1672360Z ##[error]core/capabilities/integration_tests/framework/don.go:368:15: node.AddJobV2 undefined (type *capabilityNode has no field or method AddJobV2) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1673392Z 		err := node.AddJobV2(ctx, j)
Golang Lint (.)	2025-02-25T05:10:04.1673786Z 		 ^
Golang Lint (.)	2025-02-25T05:10:04.1675290Z ##[error]core/cmd/admin_commands.go:147:12: p.Role undefined (type *AdminUsersPresenter has no field or method Role) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1676121Z 		string(p.Role),
Golang Lint (.)	2025-02-25T05:10:04.1676451Z 		 ^
Golang Lint (.)	2025-02-25T05:10:04.1678059Z ##[error]core/cmd/admin_commands.go:148:5: p.HasActiveApiToken undefined (type *AdminUsersPresenter has no field or method HasActiveApiToken) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1679160Z 		p.HasActiveApiToken,
Golang Lint (.)	2025-02-25T05:10:04.1679487Z 		 ^
Golang Lint (.)	2025-02-25T05:10:04.1681000Z ##[error]core/cmd/admin_commands.go:149:5: p.CreatedAt undefined (type *AdminUsersPresenter has no field or method CreatedAt) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1681914Z 		p.CreatedAt.String(),
Golang Lint (.)	2025-02-25T05:10:04.1682237Z 		 ^
Golang Lint (.)	2025-02-25T05:10:04.1683653Z ##[error]core/cmd/admin_commands.go:150:5: p.UpdatedAt undefined (type *AdminUsersPresenter has no field or method UpdatedAt) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1684535Z 		p.UpdatedAt.String(),
Golang Lint (.)	2025-02-25T05:10:04.1684852Z 		 ^
Golang Lint (.)	2025-02-25T05:10:04.1686083Z ##[error]core/cmd/admin_commands.go:219:29: user.Email undefined (type AdminUsersPresenter has no field or method Email) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1687044Z 		if strings.EqualFold(user.Email, c.String("email")) {
Golang Lint (.)	2025-02-25T05:10:04.1687644Z 		 ^
Golang Lint (.)	2025-02-25T05:10:04.1688927Z ##[error]core/cmd/admin_commands.go:220:75: user.Email undefined (type AdminUsersPresenter has no field or method Email) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1690332Z 			return s.errorOut(fmt.Errorf("user with email %s already exists", user.Email))
Golang Lint (.)	2025-02-25T05:10:04.1691955Z 			 ^
Golang Lint (.)	2025-02-25T05:10:04.1693368Z ##[error]core/cmd/aptos_keys_commands.go:30:5: p.PubKey undefined (type *AptosKeyPresenter has no field or method PubKey) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1694194Z 		p.PubKey,
Golang Lint (.)	2025-02-25T05:10:04.1694473Z 		 ^
Golang Lint (.)	2025-02-25T05:10:04.1695677Z ##[error]core/cmd/blocks_commands.go:100:20: p.EVMChainID undefined (type *LCAPresenter has no field or method EVMChainID) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1697009Z 	return []string{p.EVMChainID.String(), p.Hash, strconv.FormatInt(p.BlockNumber, 10)}
Golang Lint (.)	2025-02-25T05:10:04.1697677Z 	 ^
Golang Lint (.)	2025-02-25T05:10:04.1699079Z ##[error]core/cmd/bridge_commands.go:50:37: p.Confirmations undefined (type *BridgePresenter has no field or method Confirmations) (typecheck)
Golang Lint (.)	2025-02-25T05:10:04.1700069Z 	return strconv.FormatUint(uint64(p.Confirmations), 10)
Golang Lint (.)	2025-02-25T05:10:04.1700917Z 	 ^
Golang Lint (.)	2025-02-25T05:10:04.1702152Z ##[error]
</cicore>

@harry-anderson harry-anderson requested a review from jmank88 June 30, 2025 09:14
Comment on lines 40 to 41
RouterRateLimitterPeriod = 1 * time.Minute
RouterRateLimitterLimit = 1000
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused? Otherwise, spelling:

Suggested change
RouterRateLimitterPeriod = 1 * time.Minute
RouterRateLimitterLimit = 1000
RouterRateLimiterPeriod = 1 * time.Minute
RouterRateLimiterLimit = 1000

And could use a https://pkg.go.dev/golang.org/x/time/rate#Limiter which has some helpers for specifying the rate and supports bursts.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, removed 🙂

Comment on lines 375 to 376
); err != nil {
return clsessions.ErrUserSessionExpired
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this confirm sql.ErrNoRows or something to rule out other kinds of errors?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes we should catch that case, updated now to propagate up the error if its not the expected NoRows

Comment on lines 468 to 486
if err := sqlutil.TransactDataSource(ctx, oi.ds, nil, func(tx sqlutil.DataSource) error {
return tx.GetContext(ctx, &localAdminUser, SQLSelectUserbyEmail, user.Email)
}); err != nil {
oi.lggr.Infof("Can not change password, local user with email not found in users table: %s, err: %v", user.Email, err)
return clsessions.ErrNotSupported
}

// User is local admin, save new password
hashedPassword, err := utils.HashPassword(newPassword)
if err != nil {
return err
}
if err := sqlutil.TransactDataSource(ctx, oi.ds, nil, func(tx sqlutil.DataSource) error {
sql := "UPDATE users SET hashed_password = $1, updated_at = now() WHERE email = $2 RETURNING *"
return tx.GetContext(ctx, user, sql, hashedPassword, user.Email)
}); err != nil {
oi.lggr.Errorf("unable to set password for user: %s, err: %v", user.Email, err)
return errors.New("unable to save password")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We should be able to skip the transactions in cases with a single statement

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Simplified now 🙂

Comment on lines 658 to 661
const constantTimeEmailLength = 256

func constantTimeEmailCompare(left, right string) bool {
length := mathutil.Max(constantTimeEmailLength, len(left), len(right))
Copy link
Contributor

Choose a reason for hiding this comment

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

nit/ this is valid if you want to declare it in the narrowest scope:

Suggested change
const constantTimeEmailLength = 256
func constantTimeEmailCompare(left, right string) bool {
length := mathutil.Max(constantTimeEmailLength, len(left), len(right))
func constantTimeEmailCompare(left, right string) bool {
const constantTimeEmailLength = 256
length := mathutil.Max(constantTimeEmailLength, len(left), len(right))```

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nice, done!

// User is local admin, save new password
hashedPassword, err := utils.HashPassword(newPassword)
if err != nil {
return err
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe worth wrapping?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Logged and created new user facing error in the case this is hit

)
if err != nil {
oi.lggr.Errorf("unable to create new session in oidc_sessions table %v", err)
return "", fmt.Errorf("error creating local LDAP session: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return "", fmt.Errorf("error creating local LDAP session: %w", err)
return "", fmt.Errorf("error creating local OIDC session: %w", err)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hehe nice catch

@CL-Andrew
Copy link
Collaborator Author

Merged operator-ui with support for optional OIDC login button: https://github.com/smartcontractkit/operator-ui/releases/tag/v0.8.0-371c5cf - release cut

@CL-Andrew
Copy link
Collaborator Author

Documentation PR: smartcontractkit/documentation#2790

Comment on lines 282 to 286
err := sqlutil.TransactDataSource(ctx, oi.ds, nil, func(tx sqlutil.DataSource) error {
return tx.GetContext(ctx, &foundUser, SQLSelectUserbyEmail, email)
})

if err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
err := sqlutil.TransactDataSource(ctx, oi.ds, nil, func(tx sqlutil.DataSource) error {
return tx.GetContext(ctx, &foundUser, SQLSelectUserbyEmail, email)
})
if err != nil {
if err := tx.GetContext(ctx, &foundUser, SQLSelectUserbyEmail, email); err != nil {

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

👍

Comment on lines 345 to 348
if err := sqlutil.TransactDataSource(ctx, oi.ds, nil, func(tx sqlutil.DataSource) error {
sql := "SELECT * FROM users ORDER BY email ASC;"
return tx.SelectContext(ctx, &returnUsers, sql)
}); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if err := sqlutil.TransactDataSource(ctx, oi.ds, nil, func(tx sqlutil.DataSource) error {
sql := "SELECT * FROM users ORDER BY email ASC;"
return tx.SelectContext(ctx, &returnUsers, sql)
}); err != nil {
if err := tx.SelectContext(ctx, &returnUsers, "SELECT * FROM users ORDER BY email ASC;"); err != nil {

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

👍

@cl-sonarqube-production
Copy link

@harry-anderson harry-anderson self-requested a review July 1, 2025 02:27
@harry-anderson harry-anderson added this pull request to the merge queue Jul 1, 2025
Merged via the queue into develop with commit 9127611 Jul 1, 2025
164 of 165 checks passed
@harry-anderson harry-anderson deleted the oidc-support branch July 1, 2025 02:51
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.

3 participants