Skip to content

Commit

Permalink
feat: Support for OAuth callback proxy.
Browse files Browse the repository at this point in the history
This solves a problem whereby Google et al. OAuth consent screen
indicates you're logging into the domain where GoTrue is hosted
rather than the application's domain.

GoTrue client's may now optionally supply a "proxy" param that
is provided to external providers as the callback, rather than the
GOTRUE_EXTERNAL_<PROVIDER>_REDIRECT_URI. The proxy end-point lives
at the app's domain, and simply redirects back to GoTrue (or
rather Kong).
  • Loading branch information
Benjamin-Dobell committed Oct 3, 2022
1 parent d65ba60 commit 2581929
Show file tree
Hide file tree
Showing 19 changed files with 173 additions and 50 deletions.
14 changes: 14 additions & 0 deletions api/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const (
userKey = contextKey("user")
sessionKey = contextKey("session")
externalReferrerKey = contextKey("external_referrer")
externalProxyKey = contextKey("external_proxy")
functionHooksKey = contextKey("function_hooks")
adminUserKey = contextKey("admin_user")
oauthTokenKey = contextKey("oauth_token") // for OAuth1.0, also known as request token
Expand Down Expand Up @@ -146,6 +147,19 @@ func getExternalReferrer(ctx context.Context) string {
return obj.(string)
}

func withExternalProxy(ctx context.Context, proxy string) context.Context {
return context.WithValue(ctx, externalProxyKey, proxy)
}

func getExternalProxy(ctx context.Context) string {
obj := ctx.Value(externalProxyKey)
if obj == nil {
return ""
}

return obj.(string)
}

// getFunctionHooks reads the request ID from the context.
func getFunctionHooks(ctx context.Context) map[string][]string {
obj := ctx.Value(functionHooksKey)
Expand Down
45 changes: 28 additions & 17 deletions api/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type ExternalProviderClaims struct {
Provider string `json:"provider"`
InviteToken string `json:"invite_token,omitempty"`
Referrer string `json:"referrer,omitempty"`
Proxy string `json:"proxy,omitempty"`
}

// ExternalSignupParams are the parameters the Signup endpoint accepts
Expand All @@ -44,6 +45,11 @@ func (a *API) ExternalProviderRedirect(w http.ResponseWriter, r *http.Request) e
query := r.URL.Query()
providerType := query.Get("provider")
scopes := query.Get("scopes")
proxy := query.Get("proxy")

if proxy != "" {
ctx = withExternalProxy(ctx, proxy)
}

p, err := a.Provider(ctx, providerType, scopes, &query)
if err != nil {
Expand Down Expand Up @@ -76,6 +82,7 @@ func (a *API) ExternalProviderRedirect(w http.ResponseWriter, r *http.Request) e
Provider: providerType,
InviteToken: inviteToken,
Referrer: redirectURL,
Proxy: proxy,
})
tokenString, err := token.SignedString([]byte(config.JWT.Secret))
if err != nil {
Expand Down Expand Up @@ -393,6 +400,9 @@ func (a *API) loadExternalState(ctx context.Context, state string) (context.Cont
if claims.Referrer != "" {
ctx = withExternalReferrer(ctx, claims.Referrer)
}
if claims.Proxy != "" {
ctx = withExternalProxy(ctx, claims.Proxy)
}

ctx = withExternalProviderType(ctx, claims.Provider)
return withSignature(ctx, state), nil
Expand All @@ -402,42 +412,43 @@ func (a *API) loadExternalState(ctx context.Context, state string) (context.Cont
func (a *API) Provider(ctx context.Context, name string, scopes string, query *url.Values) (provider.Provider, error) {
config := a.config
name = strings.ToLower(name)
proxyURL := getExternalProxy(ctx)

switch name {
case "apple":
return provider.NewAppleProvider(config.External.Apple)
return provider.NewAppleProvider(config.External.Apple, proxyURL)
case "azure":
return provider.NewAzureProvider(config.External.Azure, scopes)
return provider.NewAzureProvider(config.External.Azure, proxyURL, scopes)
case "bitbucket":
return provider.NewBitbucketProvider(config.External.Bitbucket)
return provider.NewBitbucketProvider(config.External.Bitbucket, proxyURL)
case "discord":
return provider.NewDiscordProvider(config.External.Discord, scopes)
return provider.NewDiscordProvider(config.External.Discord, proxyURL, scopes)
case "github":
return provider.NewGithubProvider(config.External.Github, scopes)
return provider.NewGithubProvider(config.External.Github, proxyURL, scopes)
case "gitlab":
return provider.NewGitlabProvider(config.External.Gitlab, scopes)
return provider.NewGitlabProvider(config.External.Gitlab, proxyURL, scopes)
case "google":
return provider.NewGoogleProvider(config.External.Google, scopes)
return provider.NewGoogleProvider(config.External.Google, proxyURL, scopes)
case "keycloak":
return provider.NewKeycloakProvider(config.External.Keycloak, scopes)
return provider.NewKeycloakProvider(config.External.Keycloak, proxyURL, scopes)
case "linkedin":
return provider.NewLinkedinProvider(config.External.Linkedin, scopes)
return provider.NewLinkedinProvider(config.External.Linkedin, proxyURL, scopes)
case "facebook":
return provider.NewFacebookProvider(config.External.Facebook, scopes)
return provider.NewFacebookProvider(config.External.Facebook, proxyURL, scopes)
case "notion":
return provider.NewNotionProvider(config.External.Notion)
return provider.NewNotionProvider(config.External.Notion, proxyURL)
case "spotify":
return provider.NewSpotifyProvider(config.External.Spotify, scopes)
return provider.NewSpotifyProvider(config.External.Spotify, proxyURL, scopes)
case "slack":
return provider.NewSlackProvider(config.External.Slack, scopes)
return provider.NewSlackProvider(config.External.Slack, proxyURL, scopes)
case "twitch":
return provider.NewTwitchProvider(config.External.Twitch, scopes)
return provider.NewTwitchProvider(config.External.Twitch, proxyURL, scopes)
case "twitter":
return provider.NewTwitterProvider(config.External.Twitter, scopes)
return provider.NewTwitterProvider(config.External.Twitter, proxyURL, scopes)
case "workos":
return provider.NewWorkOSProvider(config.External.WorkOS, query)
return provider.NewWorkOSProvider(config.External.WorkOS, proxyURL, query)
case "zoom":
return provider.NewZoomProvider(config.External.Zoom)
return provider.NewZoomProvider(config.External.Zoom, proxyURL)
default:
return nil, fmt.Errorf("Provider %s could not be found", name)
}
Expand Down
9 changes: 7 additions & 2 deletions api/provider/apple.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,17 @@ type idTokenClaims struct {
}

// NewAppleProvider creates a Apple account provider.
func NewAppleProvider(ext conf.OAuthProviderConfiguration) (OAuthProvider, error) {
func NewAppleProvider(ext conf.OAuthProviderConfiguration, proxyURL string) (OAuthProvider, error) {
if err := ext.Validate(); err != nil {
return nil, err
}

authHost := chooseHost(ext.URL, defaultAppleAPIBase)
redirectURL := proxyURL

if redirectURL == "" {
redirectURL = ext.RedirectURI
}

return &AppleProvider{
Config: &oauth2.Config{
Expand All @@ -74,7 +79,7 @@ func NewAppleProvider(ext conf.OAuthProviderConfiguration) (OAuthProvider, error
scopeEmail,
scopeName,
},
RedirectURL: ext.RedirectURI,
RedirectURL: redirectURL,
},
UserInfoURL: authHost + idTokenVerificationKeyEndpoint,
}, nil
Expand Down
10 changes: 8 additions & 2 deletions api/provider/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type azureUser struct {
}

// NewAzureProvider creates a Azure account provider.
func NewAzureProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
func NewAzureProvider(ext conf.OAuthProviderConfiguration, proxyURL string, scopes string) (OAuthProvider, error) {
if err := ext.Validate(); err != nil {
return nil, err
}
Expand All @@ -40,6 +40,12 @@ func NewAzureProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuth
oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...)
}

redirectURL := proxyURL

if redirectURL == "" {
redirectURL = ext.RedirectURI
}

return &azureProvider{
Config: &oauth2.Config{
ClientID: ext.ClientID,
Expand All @@ -48,7 +54,7 @@ func NewAzureProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuth
AuthURL: authHost + "/oauth2/v2.0/authorize",
TokenURL: authHost + "/oauth2/v2.0/token",
},
RedirectURL: ext.RedirectURI,
RedirectURL: redirectURL,
Scopes: oauthScopes,
},
APIPath: apiPath,
Expand Down
9 changes: 7 additions & 2 deletions api/provider/bitbucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,18 @@ type bitbucketEmails struct {
}

// NewBitbucketProvider creates a Bitbucket account provider.
func NewBitbucketProvider(ext conf.OAuthProviderConfiguration) (OAuthProvider, error) {
func NewBitbucketProvider(ext conf.OAuthProviderConfiguration, proxyURL string) (OAuthProvider, error) {
if err := ext.Validate(); err != nil {
return nil, err
}

authHost := chooseHost(ext.URL, defaultBitbucketAuthBase)
apiPath := chooseHost(ext.URL, defaultBitbucketAPIBase) + "/2.0"
redirectURL := proxyURL

if redirectURL == "" {
redirectURL = ext.RedirectURI
}

return &bitbucketProvider{
Config: &oauth2.Config{
Expand All @@ -53,7 +58,7 @@ func NewBitbucketProvider(ext conf.OAuthProviderConfiguration) (OAuthProvider, e
AuthURL: authHost + "/site/oauth2/authorize",
TokenURL: authHost + "/site/oauth2/access_token",
},
RedirectURL: ext.RedirectURI,
RedirectURL: redirectURL,
Scopes: []string{"account", "email"},
},
APIPath: apiPath,
Expand Down
10 changes: 8 additions & 2 deletions api/provider/discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type discordUser struct {
}

// NewDiscordProvider creates a Discord account provider.
func NewDiscordProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
func NewDiscordProvider(ext conf.OAuthProviderConfiguration, proxyURL string, scopes string) (OAuthProvider, error) {
if err := ext.Validate(); err != nil {
return nil, err
}
Expand All @@ -46,6 +46,12 @@ func NewDiscordProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAu
oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...)
}

redirectURL := proxyURL

if redirectURL == "" {
redirectURL = ext.RedirectURI
}

return &discordProvider{
Config: &oauth2.Config{
ClientID: ext.ClientID,
Expand All @@ -55,7 +61,7 @@ func NewDiscordProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAu
TokenURL: apiPath + "/oauth2/token",
},
Scopes: oauthScopes,
RedirectURL: ext.RedirectURI,
RedirectURL: redirectURL,
},
APIPath: apiPath,
}, nil
Expand Down
8 changes: 7 additions & 1 deletion api/provider/facebook.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type facebookUser struct {
}

// NewFacebookProvider creates a Facebook account provider.
func NewFacebookProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
func NewFacebookProvider(ext conf.OAuthProviderConfiguration, proxyURL string, scopes string) (OAuthProvider, error) {
if err := ext.Validate(); err != nil {
return nil, err
}
Expand All @@ -54,6 +54,12 @@ func NewFacebookProvider(ext conf.OAuthProviderConfiguration, scopes string) (OA
oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...)
}

redirectURL := proxyURL

if redirectURL == "" {
redirectURL = ext.RedirectURI
}

return &facebookProvider{
Config: &oauth2.Config{
ClientID: ext.ClientID,
Expand Down
10 changes: 8 additions & 2 deletions api/provider/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type githubUserEmail struct {
}

// NewGithubProvider creates a Github account provider.
func NewGithubProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
func NewGithubProvider(ext conf.OAuthProviderConfiguration, proxyURL string, scopes string) (OAuthProvider, error) {
if err := ext.Validate(); err != nil {
return nil, err
}
Expand All @@ -56,6 +56,12 @@ func NewGithubProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAut
oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...)
}

redirectURL := proxyURL

if redirectURL == "" {
redirectURL = ext.RedirectURI
}

return &githubProvider{
Config: &oauth2.Config{
ClientID: ext.ClientID,
Expand All @@ -64,7 +70,7 @@ func NewGithubProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAut
AuthURL: authHost + "/login/oauth/authorize",
TokenURL: authHost + "/login/oauth/access_token",
},
RedirectURL: ext.RedirectURI,
RedirectURL: redirectURL,
Scopes: oauthScopes,
},
APIHost: apiHost,
Expand Down
10 changes: 8 additions & 2 deletions api/provider/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type gitlabUserEmail struct {
}

// NewGitlabProvider creates a Gitlab account provider.
func NewGitlabProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
func NewGitlabProvider(ext conf.OAuthProviderConfiguration, proxyURL string, scopes string) (OAuthProvider, error) {
if err := ext.Validate(); err != nil {
return nil, err
}
Expand All @@ -47,6 +47,12 @@ func NewGitlabProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAut
}

host := chooseHost(ext.URL, defaultGitLabAuthBase)
redirectURL := proxyURL

if redirectURL == "" {
redirectURL = ext.RedirectURI
}

return &gitlabProvider{
Config: &oauth2.Config{
ClientID: ext.ClientID,
Expand All @@ -55,7 +61,7 @@ func NewGitlabProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAut
AuthURL: host + "/oauth/authorize",
TokenURL: host + "/oauth/token",
},
RedirectURL: ext.RedirectURI,
RedirectURL: redirectURL,
Scopes: oauthScopes,
},
Host: host,
Expand Down
10 changes: 8 additions & 2 deletions api/provider/google.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type googleUser struct {
}

// NewGoogleProvider creates a Google account provider.
func NewGoogleProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
func NewGoogleProvider(ext conf.OAuthProviderConfiguration, proxyURL string, scopes string) (OAuthProvider, error) {
if err := ext.Validate(); err != nil {
return nil, err
}
Expand All @@ -45,6 +45,12 @@ func NewGoogleProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAut
oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...)
}

redirectURL := proxyURL

if redirectURL == "" {
redirectURL = ext.RedirectURI
}

return &googleProvider{
Config: &oauth2.Config{
ClientID: ext.ClientID,
Expand All @@ -54,7 +60,7 @@ func NewGoogleProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAut
TokenURL: authHost + "/o/oauth2/token",
},
Scopes: oauthScopes,
RedirectURL: ext.RedirectURI,
RedirectURL: redirectURL,
},
APIPath: apiPath,
}, nil
Expand Down
10 changes: 8 additions & 2 deletions api/provider/keycloak.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type keycloakUser struct {
}

// NewKeycloakProvider creates a Keycloak account provider.
func NewKeycloakProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
func NewKeycloakProvider(ext conf.OAuthProviderConfiguration, proxyURL string, scopes string) (OAuthProvider, error) {
if err := ext.Validate(); err != nil {
return nil, err
}
Expand All @@ -46,6 +46,12 @@ func NewKeycloakProvider(ext conf.OAuthProviderConfiguration, scopes string) (OA
ext.URL = ext.URL[:extURLlen-1]
}

redirectURL := proxyURL

if redirectURL == "" {
redirectURL = ext.RedirectURI
}

return &keycloakProvider{
Config: &oauth2.Config{
ClientID: ext.ClientID,
Expand All @@ -54,7 +60,7 @@ func NewKeycloakProvider(ext conf.OAuthProviderConfiguration, scopes string) (OA
AuthURL: ext.URL + "/protocol/openid-connect/auth",
TokenURL: ext.URL + "/protocol/openid-connect/token",
},
RedirectURL: ext.RedirectURI,
RedirectURL: redirectURL,
Scopes: oauthScopes,
},
Host: ext.URL,
Expand Down

0 comments on commit 2581929

Please sign in to comment.