Files
hanko/backend/thirdparty/provider_facebook.go
Frederic Jahn 7fe0862369 PKCE OAuth flow (#2266)
* feat: add auth prompt config option

* feat: add pkce oauth flow

When the oauth flow is initialized with a `code_verifier` the state cookie is optional and on hanko_token exchange the client must also send the `code_verifier` in addition to the `hanko_token`.

* fix: fix runtime errors & tests
2025-10-13 14:28:42 +02:00

137 lines
3.6 KiB
Go

package thirdparty
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"github.com/teamhanko/hanko/backend/v2/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, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
return f.oauthConfig.Exchange(context.Background(), code, opts...)
}
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
}
func (f facebookProvider) GetPromptParam() string {
if f.config.Prompt != "" {
return f.config.Prompt
}
return "consent"
}