Files
hanko/backend/thirdparty/provider_facebook.go
Lennart Fleischmann 062aee4d45 fix: SAML issues (#2041)
Rename identities table columns for more clarity. Rename parameters,
arguments etc. to accommodate these changes.
    
Change that the SAML provider domain is persisted in the identities
table as the provider ID. Use the SAML Entity ID/Issuer ID of the
IdP instead.
    
Introduce saml identity entity (including migrations and a persister)
as a specialization of an identity to allow for determining the
correct provider name to return to the client/frontend and for assisting
in determining whether an identity is a SAML identity (i.e. SAML
identities should have a corresponding SAML Identity instance while
OAuth/OIDC entities do not).
2025-01-31 14:17:52 +01:00

130 lines
3.5 KiB
Go

package thirdparty
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"github.com/teamhanko/hanko/backend/config"
"golang.org/x/oauth2"
"net/url"
)
const (
FacebookAuthBase = "https://www.facebook.com"
FacebookAPIBase = "https://graph.facebook.com"
FacebookOauthAuthEndpoint = FacebookAuthBase + "/v21.0/dialog/oauth"
FacebookOauthTokenEndpoint = FacebookAPIBase + "/v21.0/oauth/access_token"
FacebookUserInfoEndpoint = FacebookAPIBase + "/me"
)
var DefaultFacebookScopes = []string{
"email", "public_profile",
}
type facebookProvider struct {
config config.ThirdPartyProvider
oauthConfig *oauth2.Config
}
type FacebookUser struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Picture struct {
Data struct {
URL string `json:"url"`
} `json:"data"`
} `json:"picture"`
FirstName string `json:"first_name"`
MiddleName string `json:"middle_name"`
LastName string `json:"last_name"`
}
// NewFacebookProvider creates a Facebook third-party OAuth provider.
func NewFacebookProvider(config config.ThirdPartyProvider, redirectURL string) (OAuthProvider, error) {
if !config.Enabled {
return nil, errors.New("facebook provider is disabled")
}
return &facebookProvider{
config: config,
oauthConfig: &oauth2.Config{
ClientID: config.ClientID,
ClientSecret: config.Secret,
Endpoint: oauth2.Endpoint{
AuthURL: FacebookOauthAuthEndpoint,
TokenURL: FacebookOauthTokenEndpoint,
},
Scopes: DefaultFacebookScopes,
RedirectURL: redirectURL,
},
}, nil
}
func (f facebookProvider) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
return f.oauthConfig.AuthCodeURL(state, opts...)
}
func (f facebookProvider) GetOAuthToken(code string) (*oauth2.Token, error) {
return f.oauthConfig.Exchange(context.Background(), code)
}
func (f facebookProvider) GetUserData(token *oauth2.Token) (*UserData, error) {
endpointURL, err := url.Parse(FacebookUserInfoEndpoint)
if err != nil {
return nil, err
}
endpointURLQuery := endpointURL.Query()
endpointURLQuery.Add("fields", "id,name,email,picture,first_name,middle_name,last_name")
// Calculate appsecret_proof, see:
// https://developers.facebook.com/docs/graph-api/guides/secure-requests/#appsecret_proof
hash := hmac.New(sha256.New, []byte(f.config.Secret))
hash.Write([]byte(token.AccessToken))
appsecretProof := hex.EncodeToString(hash.Sum(nil))
endpointURLQuery.Add("appsecret_proof", appsecretProof)
endpointURL.RawQuery = endpointURLQuery.Encode()
var fbUser FacebookUser
if err = makeRequest(token, f.oauthConfig, endpointURL.String(), &fbUser); err != nil {
return nil, err
}
if fbUser.Email == "" {
return nil, errors.New("unable to find email with Facebook provider")
}
data := &UserData{
Emails: []Email{
{
Email: fbUser.Email,
// Consider the email as verified because a User node only returns an email if a valid
// email address is available. See: https://developers.facebook.com/docs/graph-api/reference/user/
Verified: true,
Primary: true,
},
},
Metadata: &Claims{
Issuer: FacebookAuthBase,
Subject: fbUser.ID,
Name: fbUser.Name,
Picture: fbUser.Picture.Data.URL,
Email: fbUser.Email,
EmailVerified: true,
GivenName: fbUser.FirstName,
MiddleName: fbUser.MiddleName,
FamilyName: fbUser.LastName,
},
}
return data, nil
}
func (f facebookProvider) ID() string {
return f.config.ID
}