Skip to content

Commit

Permalink
feat: add facebook provider (#2007)
Browse files Browse the repository at this point in the history
* add sign in with facebook

* feat: add facebook provider to factory function

* feat: add facebook config defaults

* feat: use newest facebook api version

* feat: make facebook provider consistent with other providers

* feat: add check for email

We cannot assume a user always has a valid email.
Even though it is not the used "me" endpoint, see:

https://developers.facebook.com/docs/graph-api/reference/user/

* docs: elaborate comment

* fix: fix third party tests

* feat: add facebook icon

* feat: add appsecret_proof to requests w. access token

* refactor: build userinfo url programmatically

* feat: map all available name claims

---------

Co-authored-by: Prathamesh <[email protected]>
  • Loading branch information
lfleischmann and prathamesh0987 authored Jan 15, 2025
1 parent 5023a53 commit d66b267
Show file tree
Hide file tree
Showing 20 changed files with 387 additions and 5 deletions.
2 changes: 2 additions & 0 deletions backend/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ third_party:
enabled: false
microsoft:
enabled: false
facebook:
enabled: false
username:
enabled: false
optional: true
Expand Down
5 changes: 5 additions & 0 deletions backend/config/config_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ func DefaultConfig() *Config {
AllowLinking: true,
Name: "google",
},
Facebook: ThirdPartyProvider{
DisplayName: "Facebook",
AllowLinking: true,
Name: "facebook",
},
},
},
Passkey: Passkey{
Expand Down
5 changes: 4 additions & 1 deletion backend/config/config_third_party.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -375,6 +376,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 {
Expand Down
6 changes: 5 additions & 1 deletion backend/flow_api/flow/shared/hook_generate_oauth_links.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package shared

import (
"fmt"
"net/url"

"github.com/labstack/echo/v4"
"github.com/teamhanko/hanko/backend/flowpilot"
"net/url"
)

type GenerateOAuthLinks struct {
Expand Down Expand Up @@ -38,6 +39,9 @@ func (h GenerateOAuthLinks) Execute(c flowpilot.HookExecutionContext) error {
if deps.Cfg.ThirdParty.Providers.Apple.Enabled {
c.AddLink(OAuthLink("apple", h.generateHref(deps.HttpContext, "apple", returnToUrl)))
}
if deps.Cfg.ThirdParty.Providers.Facebook.Enabled {
c.AddLink(OAuthLink("facebook", h.generateHref(deps.HttpContext, "facebook", returnToUrl)))
}

return nil
}
Expand Down
9 changes: 9 additions & 0 deletions backend/handler/thirdparty_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ func (s *thirdPartySuite) TestThirdPartyHandler_Auth() {
requestedRedirectTo: "https://app.test.example",
expectedBaseURL: thirdparty.MicrosoftOAuthAuthEndpoint,
},
{
name: "successful redirect to facebook",
referer: "https://login.test.example",
enabledProviders: []string{"facebook"},
allowedRedirectURLs: []string{"https://*.test.example"},
requestedProvider: "facebook",
requestedRedirectTo: "https://app.test.example",
expectedBaseURL: thirdparty.FacebookOauthAuthEndpoint,
},
{
name: "error redirect on missing provider",
referer: "https://login.test.example",
Expand Down
7 changes: 4 additions & 3 deletions backend/handler/thirdparty_callback_error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package handler

import (
"fmt"
"github.com/h2non/gock"
"github.com/teamhanko/hanko/backend/thirdparty"
"github.com/teamhanko/hanko/backend/utils"
"net/http"
"net/http/httptest"
"testing"

"github.com/h2non/gock"
"github.com/teamhanko/hanko/backend/thirdparty"
"github.com/teamhanko/hanko/backend/utils"
)

func (s *thirdPartySuite) TestThirdPartyHandler_Callback_Error_LinkingNotAllowedForProvider() {
Expand Down
117 changes: 117 additions & 0 deletions backend/handler/thirdparty_callback_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,123 @@ func (s *thirdPartySuite) TestThirdPartyHandler_Callback_SignIn_Microsoft() {
}
}

func (s *thirdPartySuite) TestThirdPartyHandler_Callback_SignUp_Facebook() {
defer gock.Off()
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}

gock.New(thirdparty.FacebookOauthTokenEndpoint).
Post("/").
Reply(200).
JSON(map[string]string{"access_token": "fakeAccessToken"})

gock.New(thirdparty.FacebookUserInfoEndpoint).
Get("/me").
Reply(200).
JSON(&thirdparty.FacebookUser{
ID: "facebook_abcde",
Email: "[email protected]",
})

cfg := s.setUpConfig([]string{"facebook"}, []string{"https://example.com"})

state, err := thirdparty.GenerateState(cfg, "facebook", "https://example.com")
s.NoError(err)

req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/thirdparty/callback?code=abcde&state=%s", state), nil)
req.AddCookie(&http.Cookie{
Name: utils.HankoThirdpartyStateCookie,
Value: string(state),
})

c, rec := s.setUpContext(req)
handler := s.setUpHandler(cfg)

if s.NoError(handler.Callback(c)) {
s.Equal(http.StatusTemporaryRedirect, rec.Code)

s.assertLocationHeaderHasToken(rec)
s.assertStateCookieRemoved(rec)

email, err := s.Storage.GetEmailPersister().FindByAddress("[email protected]")
s.NoError(err)
s.NotNil(email)
s.True(email.IsPrimary())

user, err := s.Storage.GetUserPersister().Get(*email.UserID)
s.NoError(err)
s.NotNil(user)

identity := email.Identities.GetIdentity("facebook", "facebook_abcde")
s.NotNil(identity)

logs, lerr := s.Storage.GetAuditLogPersister().List(0, 0, nil, nil, []string{"thirdparty_signup_succeeded"}, user.ID.String(), email.Address, "", "")
s.NoError(lerr)
s.Len(logs, 1)
}
}

func (s *thirdPartySuite) TestThirdPartyHandler_Callback_SignIn_Facebook() {
defer gock.Off()
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}

err := s.LoadFixtures("../test/fixtures/thirdparty")
s.NoError(err)

gock.New(thirdparty.FacebookOauthTokenEndpoint).
Post("/").
Reply(200).
JSON(map[string]string{"access_token": "fakeAccessToken"})

gock.New(thirdparty.FacebookUserInfoEndpoint).
Get("/me").
Reply(200).
JSON(&thirdparty.FacebookUser{
ID: "facebook_abcde",
Email: "[email protected]",
})

cfg := s.setUpConfig([]string{"facebook"}, []string{"https://example.com"})

state, err := thirdparty.GenerateState(cfg, "facebook", "https://example.com")
s.NoError(err)

req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/thirdparty/callback?code=abcde&state=%s", state), nil)
req.AddCookie(&http.Cookie{
Name: utils.HankoThirdpartyStateCookie,
Value: string(state),
})

c, rec := s.setUpContext(req)
handler := s.setUpHandler(cfg)

if s.NoError(handler.Callback(c)) {
s.Equal(http.StatusTemporaryRedirect, rec.Code)

s.assertLocationHeaderHasToken(rec)
s.assertStateCookieRemoved(rec)

email, err := s.Storage.GetEmailPersister().FindByAddress("[email protected]")
s.NoError(err)
s.NotNil(email)
s.True(email.IsPrimary())

user, err := s.Storage.GetUserPersister().Get(*email.UserID)
s.NoError(err)
s.NotNil(user)

identity := email.Identities.GetIdentity("facebook", "facebook_abcde")
s.NotNil(identity)

logs, lerr := s.Storage.GetAuditLogPersister().List(0, 0, nil, nil, []string{"thirdparty_signin_succeeded"}, user.ID.String(), "", "", "")
s.NoError(lerr)
s.Len(logs, 1)
}
}

func (s *thirdPartySuite) TestThirdPartyHandler_Callback_SignUp_WithUnclaimedEmail() {
defer gock.Off()
if testing.Short() {
Expand Down
9 changes: 9 additions & 0 deletions backend/handler/thirdparty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ func (s *thirdPartySuite) setUpConfig(enabledProviders []string, allowedRedirect
Secret: "fakeClientSecret",
AllowLinking: false,
},
Facebook: config.ThirdPartyProvider{
Name: "facebook",
Enabled: false,
ClientID: "fakeClientID",
Secret: "fakeClientSecret",
AllowLinking: false,
},
},
ErrorRedirectURL: "https://error.test.example",
RedirectURL: "https://api.test.example/callback",
Expand All @@ -117,6 +124,8 @@ func (s *thirdPartySuite) setUpConfig(enabledProviders []string, allowedRedirect
cfg.ThirdParty.Providers.Discord.Enabled = true
case "microsoft":
cfg.ThirdParty.Providers.Microsoft.Enabled = true
case "facebook":
cfg.ThirdParty.Providers.Facebook.Enabled = true
}
}

Expand Down
4 changes: 4 additions & 0 deletions backend/json_schema/hanko.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,10 @@
"microsoft": {
"$ref": "#/$defs/ThirdPartyProvider",
"description": "`microsoft` contains the provider configuration for Microsoft."
},
"facebook": {
"$ref": "#/$defs/ThirdPartyProvider",
"description": "`facebook` contains the provider configuration for Facebook."
}
},
"additionalProperties": false,
Expand Down
6 changes: 6 additions & 0 deletions backend/test/fixtures/thirdparty/emails.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@
verified: false
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
- id: 967ce4a0-677d-4dc3-bacf-53d54471369c
user_id:
address: [email protected]
verified: true
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
- id: 527afce8-3b7b-41b6-b1ed-33d408c5a7bb
user_id: 43fb7e88-4d5d-4b2b-9335-391e78d7e472
address: [email protected]
Expand Down
7 changes: 7 additions & 0 deletions backend/test/fixtures/thirdparty/identities.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,10 @@
email_id: d781006b-4f55-4327-bad6-55bc34b88585
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
- id: b6b1309d-61de-4a82-b8b8-d54db0be679b
provider_id: "facebook_abcde"
provider_name: "facebook"
data: '{"email":"[email protected]","sub":"facebook_abcde"}'
email_id: d781006b-4f55-4327-bad6-55bc34b88585
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
5 changes: 5 additions & 0 deletions backend/test/fixtures/thirdparty/primary_emails.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@
user_id: 43fb7e88-4d5d-4b2b-9335-391e78d7e472
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
- id: e2beaaa9-1275-4eb5-aa28-9970b36d249e
email_id: 967ce4a0-677d-4dc3-bacf-53d54471369c
user_id: ef0a05a7-98d1-4e5a-a60f-2c5f740cd26d
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
4 changes: 4 additions & 0 deletions backend/test/fixtures/thirdparty/users.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
- id: 48df412f-a7b1-4fbc-ad2d-56bd3e103fd7
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
# user with email and facebook identity
- id: ef0a05a7-98d1-4e5a-a60f-2c5f740cd26d
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
# user with email, no identity
- id: 43fb7e88-4d5d-4b2b-9335-391e78d7e472
created_at: 2020-12-31 23:59:59
Expand Down
2 changes: 2 additions & 0 deletions backend/thirdparty/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ func getThirdPartyProvider(config config.ThirdParty, id string) (OAuthProvider,
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("unknown provider: %s", id)
}
Expand Down
Loading

0 comments on commit d66b267

Please sign in to comment.