- Drop dead OAuth2 TokenExtraParams plumbing - Drop Deel-only x-client-id header from basic-form - Follow Bitbucket workspace pagination cursor - Reject non-numeric ClickUp timestamps via strconv Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
184 lines
6.8 KiB
Go
184 lines
6.8 KiB
Go
// Copyright (c) 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 (
|
|
"maps"
|
|
"strings"
|
|
|
|
"go.gearno.de/kit/httpclient"
|
|
)
|
|
|
|
// CallbackPath is the HTTP path for the OAuth2 callback endpoint.
|
|
const CallbackPath = "/api/console/v1/connectors/complete"
|
|
|
|
// providerDefinition holds the static OAuth2 properties for a provider.
|
|
// These are intrinsic to the provider and do not vary between deployments.
|
|
// Scopes are not part of this — they are passed by the caller at
|
|
// initiate time via InitiateOptions, since the same provider may be used
|
|
// in multiple contexts requiring different scope sets.
|
|
type providerDefinition struct {
|
|
AuthURL string
|
|
TokenURL string
|
|
ExtraAuthParams map[string]string
|
|
TokenEndpointAuth string // "post-form" (default), "basic-form", or "basic-json"
|
|
SupportsIncrementalAuth bool
|
|
// RequiresPKCE enables RFC 7636 PKCE (S256) on the authorization
|
|
// request and replays the verifier on the token exchange. Default
|
|
// false; existing providers are unaffected.
|
|
RequiresPKCE bool
|
|
}
|
|
|
|
// providerDefinitions maps provider names to their static OAuth2 definitions.
|
|
// Only ClientID and ClientSecret come from deployment config.
|
|
var (
|
|
providerDefinitions = map[string]providerDefinition{
|
|
"SLACK": {
|
|
AuthURL: "https://slack.com/oauth/v2/authorize",
|
|
TokenURL: "https://slack.com/api/oauth.v2.access",
|
|
},
|
|
"HUBSPOT": {
|
|
AuthURL: "https://app.hubspot.com/oauth/authorize",
|
|
TokenURL: "https://api.hubapi.com/oauth/v1/token",
|
|
},
|
|
"DOCUSIGN": {
|
|
AuthURL: "https://account.docusign.com/oauth/auth",
|
|
TokenURL: "https://account.docusign.com/oauth/token",
|
|
TokenEndpointAuth: "basic-form",
|
|
},
|
|
"NOTION": {
|
|
AuthURL: "https://api.notion.com/v1/oauth/authorize",
|
|
TokenURL: "https://api.notion.com/v1/oauth/token",
|
|
ExtraAuthParams: map[string]string{"owner": "user"},
|
|
TokenEndpointAuth: "basic-json",
|
|
},
|
|
"GITHUB": {
|
|
AuthURL: "https://github.com/login/oauth/authorize",
|
|
TokenURL: "https://github.com/login/oauth/access_token",
|
|
},
|
|
"SENTRY": {
|
|
AuthURL: "https://sentry.io/oauth/authorize/",
|
|
TokenURL: "https://sentry.io/oauth/token/",
|
|
},
|
|
"INTERCOM": {
|
|
AuthURL: "https://app.intercom.com/oauth",
|
|
TokenURL: "https://api.intercom.io/auth/eagle/token",
|
|
},
|
|
"BREX": {
|
|
AuthURL: "https://accounts-api.brex.com/oauth2/default/v1/authorize",
|
|
TokenURL: "https://accounts-api.brex.com/oauth2/default/v1/token",
|
|
},
|
|
"GOOGLE_WORKSPACE": {
|
|
AuthURL: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
TokenURL: "https://oauth2.googleapis.com/token",
|
|
ExtraAuthParams: map[string]string{
|
|
"access_type": "offline",
|
|
"prompt": "consent",
|
|
},
|
|
SupportsIncrementalAuth: true,
|
|
},
|
|
"MICROSOFT_365": {
|
|
AuthURL: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
TokenURL: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
ExtraAuthParams: map[string]string{
|
|
"prompt": "consent",
|
|
},
|
|
},
|
|
"LINEAR": {
|
|
AuthURL: "https://linear.app/oauth/authorize",
|
|
TokenURL: "https://api.linear.app/oauth/token",
|
|
},
|
|
"GITLAB": {
|
|
AuthURL: "https://gitlab.com/oauth/authorize",
|
|
TokenURL: "https://gitlab.com/oauth/token",
|
|
},
|
|
// Bitbucket scopes are pinned on the OAuth consumer at registration
|
|
// time (`account` for workspace membership). They are not passed in
|
|
// the authorize URL and not configured here.
|
|
"BITBUCKET": {
|
|
AuthURL: "https://bitbucket.org/site/oauth2/authorize",
|
|
TokenURL: "https://bitbucket.org/site/oauth2/access_token",
|
|
},
|
|
"HEROKU": {
|
|
AuthURL: "https://id.heroku.com/oauth/authorize",
|
|
TokenURL: "https://id.heroku.com/oauth/token",
|
|
},
|
|
"PAGERDUTY": {
|
|
AuthURL: "https://identity.pagerduty.com/oauth/authorize",
|
|
TokenURL: "https://identity.pagerduty.com/oauth/token",
|
|
RequiresPKCE: true,
|
|
},
|
|
"ASANA": {
|
|
AuthURL: "https://app.asana.com/-/oauth_authorize",
|
|
TokenURL: "https://app.asana.com/-/oauth_token",
|
|
},
|
|
"NETLIFY": {
|
|
AuthURL: "https://app.netlify.com/authorize",
|
|
TokenURL: "https://api.netlify.com/oauth/token",
|
|
},
|
|
"CLICKUP": {
|
|
AuthURL: "https://app.clickup.com/api",
|
|
TokenURL: "https://api.clickup.com/api/v2/oauth/token",
|
|
},
|
|
// Vercel uses a templated AuthURL: the operator supplies an
|
|
// `integration-slug` config field which is resolved into the
|
|
// "{integration_slug}" placeholder by ApplyProviderDefaults.
|
|
// Vercel does not use OAuth scopes — capabilities are pinned on
|
|
// the integration registration in the Vercel dashboard.
|
|
"VERCEL": {
|
|
AuthURL: "https://vercel.com/integrations/{integration_slug}/new",
|
|
TokenURL: "https://api.vercel.com/v2/oauth/access_token",
|
|
},
|
|
"MONDAY": {
|
|
AuthURL: "https://auth.monday.com/oauth2/authorize",
|
|
TokenURL: "https://auth.monday.com/oauth2/token",
|
|
},
|
|
}
|
|
)
|
|
|
|
// ApplyProviderDefaults sets the redirect URI and applies static provider
|
|
// defaults (auth URL, token URL, extra params, token endpoint auth) onto
|
|
// an OAuth2Connector, and wires an SSRF-protected HTTP client for the
|
|
// token exchange request. Call this before registering the connector.
|
|
func ApplyProviderDefaults(provider string, redirectURI string, c *OAuth2Connector) {
|
|
c.RedirectURI = redirectURI
|
|
c.HTTPClient = httpclient.DefaultClient(httpclient.WithSSRFProtection())
|
|
|
|
if def, ok := providerDefinitions[provider]; ok {
|
|
c.AuthURL = def.AuthURL
|
|
c.TokenURL = def.TokenURL
|
|
c.TokenEndpointAuth = def.TokenEndpointAuth
|
|
c.SupportsIncrementalAuth = def.SupportsIncrementalAuth
|
|
c.RequiresPKCE = def.RequiresPKCE
|
|
|
|
// Deep copy ExtraAuthParams so per-connector mutations (e.g.
|
|
// incremental auth, scope overrides) cannot alias back into the
|
|
// shared providerDefinitions map.
|
|
if len(def.ExtraAuthParams) > 0 {
|
|
extra := make(map[string]string, len(def.ExtraAuthParams))
|
|
maps.Copy(extra, def.ExtraAuthParams)
|
|
c.ExtraAuthParams = extra
|
|
}
|
|
|
|
// Resolve operator-supplied placeholders in the static AuthURL
|
|
// (for example Vercel's "{integration_slug}"). Providers without
|
|
// placeholders are unaffected; the loop is a no-op when
|
|
// AuthURLParams is empty.
|
|
for k, v := range c.AuthURLParams {
|
|
c.AuthURL = strings.ReplaceAll(c.AuthURL, "{"+k+"}", v)
|
|
}
|
|
}
|
|
}
|