From 5ff1d6fcf432d103f6c76a640b718f3979abd7f7 Mon Sep 17 00:00:00 2001 From: devcodes9 Date: Wed, 2 Oct 2024 22:19:34 +0530 Subject: [PATCH] feat(outh2): add facebook sign in --- backend/config/config_third_party.go | 5 +- backend/go.mod | 4 +- backend/go.sum | 9 ++ backend/thirdparty/provider.go | 2 + backend/thirdparty/provider_facebook.go | 154 ++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 backend/thirdparty/provider_facebook.go diff --git a/backend/config/config_third_party.go b/backend/config/config_third_party.go index 789825124..b7bb6a7e8 100644 --- a/backend/config/config_third_party.go +++ b/backend/config/config_third_party.go @@ -3,11 +3,12 @@ package config import ( "errors" "fmt" + "strings" + "github.com/fatih/structs" "github.com/gobwas/glob" "github.com/invopop/jsonschema" orderedmap "github.com/wk8/go-ordered-map/v2" - "strings" ) type ThirdParty struct { @@ -113,6 +114,8 @@ type ThirdPartyProviders struct { LinkedIn ThirdPartyProvider `yaml:"linkedin" json:"linkedin,omitempty" koanf:"linkedin"` // `microsoft` contains the provider configuration for Microsoft. Microsoft ThirdPartyProvider `yaml:"microsoft" json:"microsoft,omitempty" koanf:"microsoft"` + // `facebook` contains the provider configuration for Facebook. + Facebook ThirdPartyProvider `yaml:"facebook" json:"facebook,omitempty" koanf:"facebook"` } func (p *ThirdPartyProviders) Validate() error { diff --git a/backend/go.mod b/backend/go.mod index 22472c0e9..fa5decc2a 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,6 +1,8 @@ module github.com/teamhanko/hanko/backend -go 1.20 +go 1.21 + +toolchain go1.23.2 require ( github.com/brianvoe/gofakeit/v6 v6.28.0 diff --git a/backend/go.sum b/backend/go.sum index 4dac7e308..57a457acd 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -6,6 +6,7 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/ClickHouse/ch-go v0.55.0 h1:jw4Tpx887YXrkyL5DfgUome/po8MLz92nz2heOQ6RjQ= github.com/ClickHouse/ch-go v0.55.0/go.mod h1:kQT2f+yp2p+sagQA/7kS6G3ukym+GQ5KAu1kuFAFDiU= github.com/ClickHouse/clickhouse-go/v2 v2.9.1 h1:IeE2bwVvAba7Yw5ZKu98bKI4NpDmykEy6jUaQdJJCk8= @@ -78,12 +79,14 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= +github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM= github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= @@ -126,6 +129,7 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -188,7 +192,9 @@ github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -222,6 +228,7 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -372,6 +379,7 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -883,6 +891,7 @@ gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/backend/thirdparty/provider.go b/backend/thirdparty/provider.go index d7e8df3d0..a82102610 100644 --- a/backend/thirdparty/provider.go +++ b/backend/thirdparty/provider.go @@ -92,6 +92,8 @@ func GetProvider(config config.ThirdParty, name string) (OAuthProvider, error) { return NewMicrosoftProvider(config.Providers.Microsoft, config.RedirectURL) case "linkedin": return NewLinkedInProvider(config.Providers.LinkedIn, config.RedirectURL) + case "facebook": + return NewFacebookProvider(config.Providers.Facebook, config.RedirectURL) default: return nil, fmt.Errorf("provider '%s' is not supported", name) } diff --git a/backend/thirdparty/provider_facebook.go b/backend/thirdparty/provider_facebook.go new file mode 100644 index 000000000..8983ad70c --- /dev/null +++ b/backend/thirdparty/provider_facebook.go @@ -0,0 +1,154 @@ +package thirdparty + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/teamhanko/hanko/backend/config" + "golang.org/x/oauth2" +) + +const ( + FacebookAPIBase = "https://graph.facebook.com" + FacebookOauthAuthEndpoint = "https://www.facebook.com/v10.0/dialog/oauth" + FacebookOauthTokenEndpoint = FacebookAPIBase + "/v10.0/oauth/access_token" + FacebookUserInfoEndpoint = FacebookAPIBase + "/me?fields=id,name,email,picture" + FacebookDebugTokenEndpoint = FacebookAPIBase + "/debug_token" +) + +var DefaultFacebookScopes = []string{ + "email", + "public_profile", +} + +type facebookProvider struct { + *oauth2.Config +} + +type FacebookUser struct { + ID string `json:"id"` + Name string `json:"name"` + Picture struct { + Data struct { + URL string `json:"url"` + } `json:"data"` + } `json:"picture"` + Email string `json:"email"` + Verified bool `json:"verified"` +} + +// TokenDebugResponse represents the response from the Facebook debug token API. +type TokenDebugResponse struct { + Data struct { + IsValid bool `json:"is_valid"` + UserID string `json:"user_id"` + AppID string `json:"app_id"` + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` + } `json:"data"` +} + +// NewFacebookProvider creates a Facebook third party provider. +func NewFacebookProvider(config config.ThirdPartyProvider, redirectURL string) (OAuthProvider, error) { + if !config.Enabled { + return nil, errors.New("facebook provider is disabled") + } + + return &facebookProvider{ + Config: &oauth2.Config{ + ClientID: config.ClientID, + ClientSecret: config.Secret, + Endpoint: oauth2.Endpoint{ + AuthURL: FacebookOauthAuthEndpoint, + TokenURL: FacebookOauthTokenEndpoint, + }, + Scopes: DefaultFacebookScopes, + RedirectURL: redirectURL, + }, + }, nil +} + +func (g facebookProvider) GetOAuthToken(code string) (*oauth2.Token, error) { + return g.Exchange(context.Background(), code) +} + +func (g facebookProvider) GetUserData(token *oauth2.Token) (*UserData, error) { + client := g.Client(context.Background(), token) + + // Get user data from Facebook API. + resp, err := client.Get(FacebookUserInfoEndpoint) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var user FacebookUser + if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { + return nil, err + } + + // Verify email by calling the token debug endpoint + emailVerified, err := g.verifyEmail(token) + if err != nil { + return nil, err + } + + user.Verified = emailVerified + + data := &UserData{} + if user.Email != "" { + data.Emails = append(data.Emails, Email{ + Email: user.Email, + Verified: user.Verified, + Primary: true, + }) + } + + if len(data.Emails) <= 0 { + return nil, errors.New("unable to find email with Facebook provider") + } + + data.Metadata = &Claims{ + Issuer: FacebookAPIBase, + Subject: user.ID, + Name: user.Name, + Picture: user.Picture.Data.URL, + Email: user.Email, + EmailVerified: user.Verified, + } + + return data, nil +} + +// verifyEmail checks if the user's email is verified using the Facebook debug token endpoint. +func (g facebookProvider) verifyEmail(token *oauth2.Token) (bool, error) { + // Build the URL for the token debug endpoint + appAccessToken := fmt.Sprintf("%s|%s", g.ClientID, g.ClientSecret) + debugTokenURL := fmt.Sprintf("%s?input_token=%s&access_token=%s", FacebookDebugTokenEndpoint, token.AccessToken, appAccessToken) + + // Make the HTTP request to the Facebook token debug endpoint + resp, err := http.Get(debugTokenURL) + if err != nil { + return false, err + } + defer resp.Body.Close() + + var debugResponse TokenDebugResponse + if err := json.NewDecoder(resp.Body).Decode(&debugResponse); err != nil { + return false, err + } + + // Check if the token is valid and if the email is verified + if !debugResponse.Data.IsValid { + return false, errors.New("invalid Facebook OAuth token") + } + + return debugResponse.Data.EmailVerified, nil +} + +func (g facebookProvider) Name() string { + return "facebook" +}