Files
probo/pkg/connector/connector.go
Aurélien Sibiril 71f6364df0 feat(connector): support OAuth2 scope preservation and incremental auth
Extend the OAuth2 connector so a reconnect can request the union of
previously granted and newly requested scopes without losing either.
Four related changes:

- Connection gains Scopes() []string so callers no longer need a type
  switch to reach the scope set. OAuth2Connection and APIKeyConnection
  implement it; SlackConnection inherits via embedding.
- OAuth2State carries RequestedScopes and CompleteWithState falls
  back to it when the provider omits the scope field (RFC 6749 §5.1
  allows this when granted equals requested). Without the fallback
  the stored Scope would be empty and the next reconnect would have
  no diff base.
- providerDefinition gains SupportsIncrementalAuth, set only for
  Google Workspace. When the flag is true and the caller passes
  InitiateOptions.IncludeGrantedScopes, the auth URL carries
  include_granted_scopes=true and the prompt=consent param is
  dropped so reuse flows see only the delta consent screen.
- InitiateOptions gains ConnectorID so the reconnect case is passed
  explicitly instead of relying on the caller to mutate r.URL.Query.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00

93 lines
2.9 KiB
Go

// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package connector
import (
"context"
"encoding/json"
"fmt"
"net/http"
"go.probo.inc/probo/pkg/gid"
)
type (
ProtocolType string
// InitiateOptions holds per-call options passed by the caller initiating
// a connector flow. Different callers may need different configurations
// for the same provider — for OAuth2, the most common case is requesting
// a different set of scopes (e.g. SCIM bridge vs access review).
InitiateOptions struct {
Scopes []string
// IncludeGrantedScopes is honored only when the provider has
// SupportsIncrementalAuth=true.
IncludeGrantedScopes bool
// ConnectorID, when set, marks this flow as a reconnect of an
// existing connector: the callback updates the row in place
// instead of creating a new one.
ConnectorID string
}
Connector interface {
Initiate(ctx context.Context, provider string, organizationID gid.GID, opts InitiateOptions, r *http.Request) (string, error)
Complete(ctx context.Context, r *http.Request) (Connection, *gid.GID, string, error) // returns: connection, organizationID, continueURL, error
}
Connection interface {
Type() ProtocolType
Client(ctx context.Context) (*http.Client, error)
Scopes() []string
json.Unmarshaler
json.Marshaler
}
)
const (
ProtocolOAuth2 ProtocolType = "OAUTH2"
ProtocolAPIKey ProtocolType = "API_KEY"
)
func UnmarshalConnection(protocol string, provider string, data []byte) (Connection, error) {
switch protocol {
case string(ProtocolOAuth2):
switch provider {
case SlackProvider:
var slackConn SlackConnection
if err := json.Unmarshal(data, &slackConn); err != nil {
return nil, fmt.Errorf("cannot unmarshal slack connection: %w", err)
}
return &slackConn, nil
default:
var conn OAuth2Connection
if err := json.Unmarshal(data, &conn); err != nil {
return nil, fmt.Errorf("cannot unmarshal oauth2 connection: %w", err)
}
return &conn, nil
}
case string(ProtocolAPIKey):
var conn APIKeyConnection
if err := json.Unmarshal(data, &conn); err != nil {
return nil, fmt.Errorf("cannot unmarshal api key connection: %w", err)
}
return &conn, nil
}
return nil, fmt.Errorf("unknown connection protocol: %s", protocol)
}