Skip to content

Refactor OAuth2 connectors: caller-owned scopes, error handling, simpler config#997

Merged
gearnode merged 17 commits intogetprobo:mainfrom
aureliensibiril:aureliensibiril/connectors-improvements
Apr 8, 2026
Merged

Refactor OAuth2 connectors: caller-owned scopes, error handling, simpler config#997
gearnode merged 17 commits intogetprobo:mainfrom
aureliensibiril:aureliensibiril/connectors-improvements

Conversation

@aureliensibiril
Copy link
Copy Markdown
Contributor

@aureliensibiril aureliensibiril commented Apr 6, 2026

Summary

This PR overhauls the OAuth2 connector layer in three coherent passes:

1. Simpler bootstrap config

  • Move OAuth2 provider definitions (auth URL, token URL, extra params, token endpoint auth) into the connector package so deployment config only carries ClientID and ClientSecret
  • Simplify ConnectorRegistry to a pure store; wire provider defaults at registration time
  • Restore GOOGLE_WORKSPACE and LINEAR provider definitions which were silently dropped during the bootstrap simplification (both were broken on this branch before this PR)

2. OAuth2 callback error handling

  • Extract the /connectors/complete handler from NewMux into dedicated functions
  • Handle OAuth2 error callbacks (RFC 6749 §4.1.2.1): log the error with provider context and redirect the user back to their continuation URL with error and error_description query params instead of falling through to the code exchange

3. Caller-owned OAuth2 scopes

The same provider may need different scopes in different contexts. Google Workspace is the canonical case: the SCIM bridge needs full directory + groups + customer + userschema (4 scopes); access review only needs to list users (2 scopes). Baking scopes into the connector at registration time made this impossible.

  • Add InitiateOptions{Scopes []string} to the Connector interface; /connectors/initiate reads repeated ?scope= query parameters and forwards them
  • Each owning Go module declares its scopes as a constant or registry:
    • pkg/accessreview/drivers/oauth2_scopes.go — per-provider scopes for access review
    • pkg/slack/oauth2_scopes.go — Slack compliance page scopes
    • pkg/iam/scim/bridge/provider/googleworkspace/oauth2_scopes.go — Google Workspace SCIM scopes
  • Scopes are surfaced via GraphQL on the type each consumer queries:
    • ConnectorProviderInfo.oauth2Scopes (access review providers list)
    • AccessSource.oauth2Scopes (reconnect flow)
    • Organization.slackOAuth2Scopes (compliance page)
    • Organization.googleWorkspaceOAuth2Scopes (SCIM bridge — on Organization, not SCIMConfiguration, since the Connect button shows when SCIM is unconfigured)
  • Frontend reads scopes from Relay fragments — zero hardcoded scope strings in TypeScript
  • Drop the dead CreateAccessSourceDialog React component, rename file to accessSourceMutations.ts

Persistence note

OAuth2 refresh tokens do not need scopes (RFC 6749 §6) — the refreshed token inherits the originally granted scopes. Scopes only matter at the auth code exchange step, so they are not persisted on the connector record.

Test plan

  • Verify each access review OAuth2 source still connects (HubSpot, GitHub, Sentry, Brex, DocuSign, Notion, Intercom, Linear, Google Workspace)
  • Verify Slack compliance page connect flow
  • Verify Google Workspace SCIM bridge connect flow
  • Deny consent on a provider and confirm the error is logged with provider context and the user is redirected back with error params
  • Verify access source reconnect flow uses the right scopes

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 7 files

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="pkg/server/api/console/v1/resolver.go">

<violation number="1" location="pkg/server/api/console/v1/resolver.go:283">
P2: OAuth2 error callback always redirects to base URL and drops state continuation/organization context.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@aureliensibiril aureliensibiril changed the title Move OAuth2 callback URL and scopes into connector package Simplify connector config and handle OAuth2 callback errors Apr 6, 2026
@aureliensibiril aureliensibiril changed the title Simplify connector config and handle OAuth2 callback errors Refactor OAuth2 connectors: caller-owned scopes, error handling, simpler config Apr 7, 2026
@aureliensibiril aureliensibiril force-pushed the aureliensibiril/connectors-improvements branch 2 times, most recently from 9d44a32 to b8ac0a0 Compare April 7, 2026 13:40
@aureliensibiril aureliensibiril force-pushed the aureliensibiril/connectors-improvements branch from 8e20152 to d509e9c Compare April 8, 2026 07:35
Centralise static OAuth2 properties (auth URL, token URL, scopes,
extra params, token endpoint auth) per provider in a single map.
This removes the need to duplicate these values in deployment
config; only ClientID and ClientSecret remain configurable.

Introduces ApplyProviderDefaults to set redirect URI and provider
defaults onto an OAuth2Connector at wiring time.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Remove provider-default mutation from Register; defaults are now
applied via ApplyProviderDefaults before registration. Rename
receiver from cr to r. Fix error messages to follow the cannot
convention. Wrap providerProbeURLs in var () block.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
All other OAuth2 properties (redirect URI, auth URL, token URL,
scopes, extra params, token endpoint auth) now come from the
connector package provider definitions at wiring time.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Compute the OAuth2 redirect URI from the base URL using the
CallbackPath constant and apply provider defaults before
registering each connector.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Drop RedirectURI, AuthURL, TokenURL, Scopes, ExtraAuthParams, and
TokenEndpointAuth from all connector config blocks. Remove
REDIRECT_URI from env var validation. Fix error wrapping in SAML
credential helpers.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Reduce closure size in NewMux by extracting the /connectors/complete
handler into a dedicated handleConnectorComplete function. Cache
r.URL.Query() into a local variable to avoid repeated parsing.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
When a provider returns an error (e.g. user denies consent), the
callback now logs the error with provider name and redirects to
the base URL with error and error_description query parameters
instead of falling through to the code exchange.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Use the ContinueURL from the state token so the user is redirected
back to where they initiated the flow instead of the root URL.
The redirect is safe because safeRedirect validates the host.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Add an InitiateOptions struct to the Connector interface so each
caller can declare the scopes it needs instead of having them baked
into the connector at registration. The HTTP handler reads repeated
?scope= query parameters from /connectors/initiate and forwards them.

Also restore GOOGLE_WORKSPACE and LINEAR provider definitions which
were silently dropped from the bootstrap config refactor.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Each module that initiates an OAuth2 flow now declares its scopes
in its own package instead of duplicating them in the frontend or
in shared connector config:

- pkg/accessreview/drivers: per-provider scopes for the access
  review drivers
- pkg/slack: scopes for the compliance page integration
- pkg/iam/scim/bridge/provider/googleworkspace: scopes for the
  SCIM provisioning bridge

These constants are surfaced to the frontend via GraphQL fields
so the frontend never hardcodes scope strings.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Add per-context fields so the frontend can read scopes from the
type that owns each connection:

- ConnectorProviderInfo.oauth2Scopes: access review providers
- AccessSource.oauth2Scopes: access review reconnect flow
- Organization.slackOAuth2Scopes (console): compliance page Slack
- Organization.googleWorkspaceOAuth2Scopes (connect): SCIM bridge

Resolvers delegate to the constants declared in each owning Go
module. The Google Workspace field lives on Organization, not on
SCIMConfiguration, so the Connect button can read it before any
SCIM configuration exists.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Each component that initiates an OAuth2 flow now reads its scopes
from the colocated Relay fragment instead of a hardcoded TypeScript
map. The five live call sites pass scopes to the backend via the
new ?scope= query parameter.

Drop the dead CreateAccessSourceDialog React component and rename
the file to accessSourceMutations.ts since only the mutation export
was used.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
eslint import-x/order sorts case-insensitively, so the lowercase
accessSourceMutations imports must precede the AddAccessSourceDialog
imports in both files.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The AccessSource.oauth2Scopes field duplicated knowledge that
naturally belongs on the Connector object that AccessSource
already exposes via its connector field. Move it to Connector so
every type that holds a connector (AccessSource, SCIMBridge, etc.)
can reach the scopes through the connector relationship.

AccessSourceRow now queries accessSource.connector { oauth2Scopes }
in its reconnect flow.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
A Google-Workspace-specific field on the generic Organization type
was future-hostile: each new SCIM bridge type would need its own
top-level field. Replace with a generic SCIMBridgeTypeInfo type
queried through Organization.scimBridgeTypes, parallel to the
ConnectorProviderInfo pattern in console/v1.

ConnectorList looks up the Google Workspace entry from the list
and passes its scopes to GoogleWorkspaceConnector as before.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
@aureliensibiril aureliensibiril force-pushed the aureliensibiril/connectors-improvements branch from d509e9c to 22ccfaa Compare April 8, 2026 09:06
@gearnode gearnode merged commit 22ccfaa into getprobo:main Apr 8, 2026
15 checks passed
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.

2 participants