package provider import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "github.com/mrjones/oauth" "github.com/supabase/auth/internal/conf" "github.com/supabase/auth/internal/utilities" "golang.org/x/oauth2" ) const ( defaultTwitterAPIBase = "api.twitter.com" requestURL = "/oauth/request_token" authenticateURL = "/oauth/authenticate" tokenURL = "/oauth/access_token" //#nosec G101 -- Not a secret value. endpointProfile = "/1.1/account/verify_credentials.json" ) // TwitterProvider stores the custom config for twitter provider type TwitterProvider struct { ClientKey string Secret string CallbackURL string AuthURL string RequestToken *oauth.RequestToken OauthVerifier string Consumer *oauth.Consumer UserInfoURL string } type twitterUser struct { UserName string `json:"screen_name"` Name string `json:"name"` AvatarURL string `json:"profile_image_url_https"` Email string `json:"email"` ID string `json:"id_str"` } // NewTwitterProvider creates a Twitter account provider. func NewTwitterProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { if err := ext.ValidateOAuth(); err != nil { return nil, err } authHost := chooseHost(ext.URL, defaultTwitterAPIBase) p := &TwitterProvider{ ClientKey: ext.ClientID[0], Secret: ext.Secret, CallbackURL: ext.RedirectURI, UserInfoURL: authHost + endpointProfile, } p.Consumer = newConsumer(p, authHost) return p, nil } // GetOAuthToken is a stub method for OAuthProvider interface, unused in OAuth1.0 protocol func (t TwitterProvider) GetOAuthToken(_ string) (*oauth2.Token, error) { return &oauth2.Token{}, nil } // GetUserData is a stub method for OAuthProvider interface, unused in OAuth1.0 protocol func (t TwitterProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { return &UserProvidedData{}, nil } // FetchUserData retrieves the user's data from the twitter provider func (t TwitterProvider) FetchUserData(ctx context.Context, tok *oauth.AccessToken) (*UserProvidedData, error) { var u twitterUser resp, err := t.Consumer.Get( t.UserInfoURL, map[string]string{"include_entities": "false", "skip_status": "true", "include_email": "true"}, tok, ) if err != nil { return nil, err } defer utilities.SafeClose(resp.Body) if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { return &UserProvidedData{}, fmt.Errorf("a %v error occurred with retrieving user from twitter", resp.StatusCode) } bits, err := io.ReadAll(resp.Body) if err != nil { return nil, err } _ = json.NewDecoder(bytes.NewReader(bits)).Decode(&u) data := &UserProvidedData{} if u.Email != "" { data.Emails = []Email{{ Email: u.Email, Verified: true, Primary: true, }} } data.Metadata = &Claims{ Issuer: t.UserInfoURL, Subject: u.ID, Name: u.Name, Picture: u.AvatarURL, PreferredUsername: u.UserName, // To be deprecated UserNameKey: u.UserName, FullName: u.Name, AvatarURL: u.AvatarURL, ProviderId: u.ID, } return data, nil } // AuthCodeURL fetches the request token from the twitter provider func (t *TwitterProvider) AuthCodeURL(state string, args ...oauth2.AuthCodeOption) string { // we do nothing with the state here as the state is passed in the requestURL step requestToken, url, err := t.Consumer.GetRequestTokenAndUrl(t.CallbackURL + "?state=" + state) if err != nil { return "" } t.RequestToken = requestToken t.AuthURL = url return t.AuthURL } func newConsumer(provider *TwitterProvider, authHost string) *oauth.Consumer { c := oauth.NewConsumer( provider.ClientKey, provider.Secret, oauth.ServiceProvider{ RequestTokenUrl: authHost + requestURL, AuthorizeTokenUrl: authHost + authenticateURL, AccessTokenUrl: authHost + tokenURL, }, ) return c } // Marshal encodes the twitter request token func (t TwitterProvider) Marshal() string { b, _ := json.Marshal(t.RequestToken) return string(b) } // Unmarshal decodes the twitter request token func (t TwitterProvider) Unmarshal(data string) (*oauth.RequestToken, error) { requestToken := &oauth.RequestToken{} err := json.NewDecoder(strings.NewReader(data)).Decode(requestToken) return requestToken, err }