fix: facebook auth
This commit is contained in:
+56
-7
@@ -17,7 +17,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
@@ -150,6 +152,11 @@ type facebook struct{ id, secret string }
|
|||||||
|
|
||||||
func (f *facebook) Name() string { return "facebook" }
|
func (f *facebook) Name() string { return "facebook" }
|
||||||
|
|
||||||
|
// Bump in lockstep with whatever version your Meta app is configured for
|
||||||
|
// (Settings → Basic → API Version). Stay on a single supported version so
|
||||||
|
// /me responses match the documented schema.
|
||||||
|
const facebookGraphVersion = "v25.0"
|
||||||
|
|
||||||
func (f *facebook) Config(redirectURL string) *oauth2.Config {
|
func (f *facebook) Config(redirectURL string) *oauth2.Config {
|
||||||
return &oauth2.Config{
|
return &oauth2.Config{
|
||||||
ClientID: f.id,
|
ClientID: f.id,
|
||||||
@@ -157,8 +164,8 @@ func (f *facebook) Config(redirectURL string) *oauth2.Config {
|
|||||||
RedirectURL: redirectURL,
|
RedirectURL: redirectURL,
|
||||||
Scopes: []string{"email", "public_profile"},
|
Scopes: []string{"email", "public_profile"},
|
||||||
Endpoint: oauth2.Endpoint{
|
Endpoint: oauth2.Endpoint{
|
||||||
AuthURL: "https://www.facebook.com/v18.0/dialog/oauth",
|
AuthURL: "https://www.facebook.com/" + facebookGraphVersion + "/dialog/oauth",
|
||||||
TokenURL: "https://graph.facebook.com/v18.0/oauth/access_token",
|
TokenURL: "https://graph.facebook.com/" + facebookGraphVersion + "/oauth/access_token",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,8 +194,18 @@ type facebookMe struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *facebook) FetchProfile(ctx context.Context, tok *oauth2.Token) (*Profile, error) {
|
func (f *facebook) FetchProfile(ctx context.Context, tok *oauth2.Token) (*Profile, error) {
|
||||||
const url = "https://graph.facebook.com/v18.0/me?fields=id,email,name,picture.type(large)"
|
// Facebook Graph API: pass the token as ?access_token=... (their
|
||||||
body, err := httpGet(ctx, tok, url)
|
// canonical mode) rather than via Bearer header. Bearer works most
|
||||||
|
// of the time but is documented as a secondary mode and has been
|
||||||
|
// reported to silently drop fields in some Graph versions.
|
||||||
|
params := url.Values{
|
||||||
|
"fields": {"id,email,name,picture.type(large)"},
|
||||||
|
"access_token": {tok.AccessToken},
|
||||||
|
}
|
||||||
|
reqURL := "https://graph.facebook.com/" + facebookGraphVersion +
|
||||||
|
"/me?" + params.Encode()
|
||||||
|
|
||||||
|
body, err := plainGet(ctx, reqURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("facebook /me: %w", err)
|
return nil, fmt.Errorf("facebook /me: %w", err)
|
||||||
}
|
}
|
||||||
@@ -199,6 +216,12 @@ func (f *facebook) FetchProfile(ctx context.Context, tok *oauth2.Token) (*Profil
|
|||||||
if u.ID == "" {
|
if u.ID == "" {
|
||||||
return nil, fmt.Errorf("facebook /me: empty id")
|
return nil, fmt.Errorf("facebook /me: empty id")
|
||||||
}
|
}
|
||||||
|
// If email permission was granted but FB still didn't return one,
|
||||||
|
// log the raw response so the operator can diagnose (commonly the
|
||||||
|
// user's FB account email is unverified — Graph API omits it silently).
|
||||||
|
if u.Email == "" {
|
||||||
|
log.Printf("oauth/facebook: /me returned no email; raw=%s", truncate(body, 512))
|
||||||
|
}
|
||||||
return &Profile{
|
return &Profile{
|
||||||
Provider: "facebook",
|
Provider: "facebook",
|
||||||
ProviderUserID: u.ID,
|
ProviderUserID: u.ID,
|
||||||
@@ -209,9 +232,35 @@ func (f *facebook) FetchProfile(ctx context.Context, tok *oauth2.Token) (*Profil
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// httpGet performs a token-authenticated GET and returns the response body.
|
func truncate(b []byte, n int) string {
|
||||||
// The provider tokens include their auth header already (Bearer for Google,
|
if len(b) <= n {
|
||||||
// query-param for Facebook), so we let oauth2.Client handle it.
|
return string(b)
|
||||||
|
}
|
||||||
|
return string(b[:n]) + "…"
|
||||||
|
}
|
||||||
|
|
||||||
|
// plainGet performs an unauthenticated GET (the caller is expected to have
|
||||||
|
// already embedded any access token in the URL). Used by Facebook because
|
||||||
|
// Graph API treats `?access_token=` as the canonical auth mode.
|
||||||
|
func plainGet(ctx context.Context, url string) ([]byte, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// httpGet performs a Bearer-authenticated GET — used by Google. Facebook
|
||||||
|
// uses plainGet with the token in a query param instead.
|
||||||
func httpGet(ctx context.Context, tok *oauth2.Token, url string) ([]byte, error) {
|
func httpGet(ctx context.Context, tok *oauth2.Token, url string) ([]byte, error) {
|
||||||
cli := oauth2.NewClient(ctx, oauth2.StaticTokenSource(tok))
|
cli := oauth2.NewClient(ctx, oauth2.StaticTokenSource(tok))
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user