diff --git a/internal/step_oauth.go b/internal/step_oauth.go index 2ce3063..23d3963 100644 --- a/internal/step_oauth.go +++ b/internal/step_oauth.go @@ -21,12 +21,16 @@ const ( googleOAuthAuthorizationURL = "https://accounts.google.com/o/oauth2/v2/auth" googleOAuthTokenURL = "https://oauth2.googleapis.com/token" googleOAuthUserinfoURL = "https://openidconnect.googleapis.com/v1/userinfo" + facebookOAuthAuthorizationURL = "https://www.facebook.com/v18.0/dialog/oauth" + facebookOAuthTokenURL = "https://graph.facebook.com/v18.0/oauth/access_token" + facebookOAuthUserinfoURL = "https://graph.facebook.com/me?fields=id,email,name" oauthDefaultReturnTo = "/auth/callback" oauthDefaultStateTTL = 15 * time.Minute oauthJSONBodyLimit = 1 << 20 ) var googleOAuthScopes = []string{"openid", "email", "profile"} +var facebookOAuthScopes = []string{"email", "public_profile"} type oauthProviderConfigStep struct { name string @@ -290,7 +294,37 @@ func oauthProviderConfig(config map[string]any, provider string) (oauthProviderC cfg.userinfoURL = override } return cfg, "" - case "facebook", "instagram", "x", "bluesky": + case "facebook": + cfg := oauthProviderConfigData{ + provider: "facebook", + clientID: oauthString(config, "facebook_oauth_client_id"), + clientSecret: oauthString(config, "facebook_oauth_client_secret"), + redirectURL: oauthString(config, "facebook_oauth_redirect_url"), + authorizationURL: facebookOAuthAuthorizationURL, + tokenURL: facebookOAuthTokenURL, + userinfoURL: facebookOAuthUserinfoURL, + scopes: append([]string(nil), facebookOAuthScopes...), + } + if cfg.clientID == "" || cfg.clientSecret == "" || cfg.redirectURL == "" { + return cfg, "facebook oauth is not fully configured" + } + if override, ok := oauthEndpointURL(config, "facebook_oauth_authorization_url", facebookOAuthAuthorizationURL, "www.facebook.com"); !ok { + return cfg, "facebook authorization_url must use https and the expected Facebook hostname unless test endpoints are explicitly enabled" + } else { + cfg.authorizationURL = override + } + if override, ok := oauthEndpointURL(config, "facebook_oauth_token_url", facebookOAuthTokenURL, "graph.facebook.com"); !ok { + return cfg, "facebook token_url must use https and the expected Facebook hostname unless test endpoints are explicitly enabled" + } else { + cfg.tokenURL = override + } + if override, ok := oauthEndpointURL(config, "facebook_oauth_userinfo_url", facebookOAuthUserinfoURL, "graph.facebook.com"); !ok { + return cfg, "facebook userinfo_url must use https and the expected Facebook hostname unless test endpoints are explicitly enabled" + } else { + cfg.userinfoURL = override + } + return cfg, "" + case "instagram", "x", "bluesky": return oauthProviderConfigData{provider: provider}, provider + " oauth provider is disabled in this release" case "": return oauthProviderConfigData{}, "missing oauth provider" diff --git a/internal/step_oauth_test.go b/internal/step_oauth_test.go index 9aa6d69..b2c63bf 100644 --- a/internal/step_oauth_test.go +++ b/internal/step_oauth_test.go @@ -386,3 +386,51 @@ func googleOAuthTestConfig(extra map[string]any) map[string]any { } return config } + +func facebookOAuthTestConfig(extra map[string]any) map[string]any { + config := map[string]any{ + "facebook_oauth_client_id": "fb-client", + "facebook_oauth_client_secret": "fb-secret", + "facebook_oauth_redirect_url": "https://app.example.com/auth/facebook/callback", + } + for k, v := range extra { + config[k] = v + } + return config +} + +func TestOAuthProviderConfig_FacebookAvailable(t *testing.T) { + step := newOAuthProviderConfigStep("test", facebookOAuthTestConfig(nil)) + result, err := step.Execute(context.Background(), nil, nil, map[string]any{"provider": "facebook"}, nil, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result.Output["available"] != true { + t.Fatalf("expected available=true, got %v", result.Output["available"]) + } + if result.Output["provider"] != "facebook" { + t.Fatalf("expected provider=facebook, got %v", result.Output["provider"]) + } + if result.Output["client_id"] != "fb-client" { + t.Fatalf("expected client_id=fb-client, got %v", result.Output["client_id"]) + } + if scopes, ok := result.Output["scopes"].([]string); !ok || len(scopes) == 0 { + t.Fatalf("expected scopes, got %v", result.Output["scopes"]) + } +} + +func TestOAuthProviderConfig_FacebookIncompleteUnavailable(t *testing.T) { + step := newOAuthProviderConfigStep("test", map[string]any{ + "facebook_oauth_client_id": "fb-client", + }) + result, err := step.Execute(context.Background(), nil, nil, map[string]any{"provider": "facebook"}, nil, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result.Output["available"] != false { + t.Fatalf("expected available=false, got %v", result.Output["available"]) + } + if result.Output["disabled_reason"] == "" || result.Output["disabled_reason"] == nil { + t.Fatalf("expected disabled_reason, got %v", result.Output["disabled_reason"]) + } +}