mirror of
https://github.com/teamhanko/hanko.git
synced 2025-10-28 23:30:15 +08:00
* 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
137 lines
3.6 KiB
Go
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"
|
|
}
|