Add bridge backend for sync
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -15,7 +15,6 @@
|
||||
package connector
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -25,8 +24,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.gearno.de/kit/httpclient"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/statelesstoken"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// NOTE: I use client_secret as a salt for the state token, it's an antipattern to
|
||||
@@ -60,6 +61,13 @@ type (
|
||||
TokenType string `json:"token_type"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// OAuth2RefreshConfig contains the OAuth2 credentials needed for token refresh.
|
||||
OAuth2RefreshConfig struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
TokenURL string
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -212,14 +220,30 @@ func (c *OAuth2Connector) CompleteWithState(ctx context.Context, r *http.Request
|
||||
return nil, nil, fmt.Errorf("cannot read token response body: %w", err)
|
||||
}
|
||||
|
||||
var oauth2Conn OAuth2Connection
|
||||
var buf bytes.Buffer
|
||||
buf.Write(body)
|
||||
err = json.NewDecoder(&buf).Decode(&oauth2Conn)
|
||||
if err != nil {
|
||||
// Parse the raw token response (OAuth2 uses expires_in, not expires_at)
|
||||
var rawToken struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &rawToken); err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot decode token response: %w", err)
|
||||
}
|
||||
|
||||
oauth2Conn := OAuth2Connection{
|
||||
AccessToken: rawToken.AccessToken,
|
||||
RefreshToken: rawToken.RefreshToken,
|
||||
TokenType: rawToken.TokenType,
|
||||
Scope: rawToken.Scope,
|
||||
}
|
||||
|
||||
// Convert expires_in (seconds) to expires_at (absolute time)
|
||||
if rawToken.ExpiresIn > 0 {
|
||||
oauth2Conn.ExpiresAt = time.Now().Add(time.Duration(rawToken.ExpiresIn) * time.Second)
|
||||
}
|
||||
|
||||
if provider == SlackProvider {
|
||||
conn, _, err := ParseSlackTokenResponse(body, oauth2Conn, organizationID)
|
||||
return conn, &payload.Data, err
|
||||
@@ -232,17 +256,88 @@ func (c *OAuth2Connection) Type() ProtocolType {
|
||||
return ProtocolOAuth2
|
||||
}
|
||||
|
||||
func (c OAuth2Connection) Client(ctx context.Context) (*http.Client, error) {
|
||||
func (c *OAuth2Connection) Client(ctx context.Context) (*http.Client, error) {
|
||||
return c.ClientWithOptions(ctx)
|
||||
}
|
||||
|
||||
// ClientWithOptions returns an HTTP client with the given options.
|
||||
// Use this to add logging and tracing to the HTTP client.
|
||||
func (c *OAuth2Connection) ClientWithOptions(ctx context.Context, opts ...httpclient.Option) (*http.Client, error) {
|
||||
transport := &oauth2Transport{
|
||||
token: c.AccessToken,
|
||||
tokenType: c.TokenType,
|
||||
underlying: httpclient.DefaultPooledTransport(opts...),
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: &oauth2Transport{
|
||||
token: c.AccessToken,
|
||||
tokenType: c.TokenType,
|
||||
underlying: http.DefaultTransport,
|
||||
},
|
||||
Transport: transport,
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// RefreshableClient returns an HTTP client that automatically refreshes the token when expired.
|
||||
// It also updates the connection's token fields if a refresh occurs.
|
||||
func (c *OAuth2Connection) RefreshableClient(ctx context.Context, cfg OAuth2RefreshConfig, opts ...httpclient.Option) (*http.Client, error) {
|
||||
if c.RefreshToken == "" {
|
||||
return c.ClientWithOptions(ctx, opts...)
|
||||
}
|
||||
|
||||
config := &oauth2.Config{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
TokenURL: cfg.TokenURL,
|
||||
},
|
||||
}
|
||||
|
||||
// Determine the token expiry
|
||||
// If ExpiresAt is zero or in the past, set expiry to force a refresh
|
||||
expiry := c.ExpiresAt
|
||||
if expiry.IsZero() || expiry.Before(time.Now()) {
|
||||
// Set expiry to the past to force oauth2 library to refresh
|
||||
expiry = time.Now().Add(-time.Hour)
|
||||
}
|
||||
|
||||
token := &oauth2.Token{
|
||||
AccessToken: c.AccessToken,
|
||||
RefreshToken: c.RefreshToken,
|
||||
Expiry: expiry,
|
||||
TokenType: c.TokenType,
|
||||
}
|
||||
|
||||
// Create an HTTP client with telemetry for the oauth2 library to use
|
||||
// This ensures token refresh requests are also logged
|
||||
baseClient := &http.Client{
|
||||
Transport: httpclient.DefaultPooledTransport(opts...),
|
||||
}
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, baseClient)
|
||||
|
||||
// Create a token source that will automatically refresh when expired
|
||||
tokenSource := config.TokenSource(ctx, token)
|
||||
|
||||
// Get the current (possibly refreshed) token
|
||||
newToken, err := tokenSource.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot refresh token: %w", err)
|
||||
}
|
||||
|
||||
// Update the connection with the potentially refreshed token
|
||||
c.AccessToken = newToken.AccessToken
|
||||
c.ExpiresAt = newToken.Expiry
|
||||
c.TokenType = newToken.TokenType
|
||||
if newToken.RefreshToken != "" {
|
||||
c.RefreshToken = newToken.RefreshToken
|
||||
}
|
||||
|
||||
// Return a client with telemetry that uses the refreshed token
|
||||
return &http.Client{
|
||||
Transport: &oauth2Transport{
|
||||
token: newToken.AccessToken,
|
||||
tokenType: newToken.TokenType,
|
||||
underlying: httpclient.DefaultPooledTransport(opts...),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c OAuth2Connection) MarshalJSON() ([]byte, error) {
|
||||
type Alias OAuth2Connection
|
||||
return json.Marshal(&struct {
|
||||
|
||||
@@ -73,3 +73,26 @@ func (cr *ConnectorRegistry) Complete(ctx context.Context, provider string, r *h
|
||||
|
||||
return connector.Complete(ctx, r)
|
||||
}
|
||||
|
||||
// GetOAuth2RefreshConfig returns the OAuth2 refresh configuration for a provider.
|
||||
// Returns nil if the provider is not found or is not an OAuth2 connector.
|
||||
func (cr *ConnectorRegistry) GetOAuth2RefreshConfig(provider string) *OAuth2RefreshConfig {
|
||||
cr.RLock()
|
||||
defer cr.RUnlock()
|
||||
|
||||
connector, ok := cr.connectors[provider]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
oauth2Connector, ok := connector.(*OAuth2Connector)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &OAuth2RefreshConfig{
|
||||
ClientID: oauth2Connector.ClientID,
|
||||
ClientSecret: oauth2Connector.ClientSecret,
|
||||
TokenURL: oauth2Connector.TokenURL,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user