Skip to content

Commit f73d1fc

Browse files
committed
feat: Adds login via twitter
1 parent aa232de commit f73d1fc

File tree

5 files changed

+126
-6
lines changed

5 files changed

+126
-6
lines changed

server/constants/oauth_info_urls.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ const (
1414
// Ref: https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api
1515
LinkedInUserInfoURL = "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,emailAddress,profilePicture(displayImage~:playableStreams))"
1616
LinkedInEmailURL = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))"
17+
18+
TwitterUserInfoURL = "https://api.twitter.com/2/users/me?user.fields=id,name,profile_image_url,username"
1719
)

server/handlers/oauth_callback.go

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func OAuthCallbackHandler() gin.HandlerFunc {
6868
case constants.AuthRecipeMethodApple:
6969
user, err = processAppleUserInfo(code)
7070
case constants.AuthRecipeMethodTwitter:
71-
user, err = processTwitterUserInfo(code)
71+
user, err = processTwitterUserInfo(code, sessionState)
7272
default:
7373
log.Info("Invalid oauth provider")
7474
err = fmt.Errorf(`invalid oauth provider`)
@@ -567,8 +567,69 @@ func processAppleUserInfo(code string) (models.User, error) {
567567
return user, err
568568
}
569569

570-
func processTwitterUserInfo(code string) (models.User, error) {
570+
func processTwitterUserInfo(code, verifier string) (models.User, error) {
571571
user := models.User{}
572-
// TODO exchange code and get user information
572+
oauth2Token, err := oauth.OAuthProviders.TwitterConfig.Exchange(oauth2.NoContext, code, oauth2.SetAuthURLParam("code_verifier", verifier))
573+
if err != nil {
574+
log.Debug("Failed to exchange code for token: ", err)
575+
return user, fmt.Errorf("invalid twitter exchange code: %s", err.Error())
576+
}
577+
578+
client := http.Client{}
579+
req, err := http.NewRequest("GET", constants.TwitterUserInfoURL, nil)
580+
if err != nil {
581+
log.Debug("Failed to create Twitter user info request: ", err)
582+
return user, fmt.Errorf("error creating Twitter user info request: %s", err.Error())
583+
}
584+
req.Header = http.Header{
585+
"Authorization": []string{fmt.Sprintf("Bearer %s", oauth2Token.AccessToken)},
586+
}
587+
588+
response, err := client.Do(req)
589+
if err != nil {
590+
log.Debug("Failed to request Twitter user info: ", err)
591+
return user, err
592+
}
593+
594+
defer response.Body.Close()
595+
body, err := ioutil.ReadAll(response.Body)
596+
if err != nil {
597+
log.Debug("Failed to read Twitter user info response body: ", err)
598+
return user, fmt.Errorf("failed to read Twitter response body: %s", err.Error())
599+
}
600+
601+
if response.StatusCode >= 400 {
602+
log.Debug("Failed to request Twitter user info: ", string(body))
603+
return user, fmt.Errorf("failed to request Twitter user info: %s", string(body))
604+
}
605+
606+
responseRawData := make(map[string]interface{})
607+
json.Unmarshal(body, &responseRawData)
608+
609+
userRawData := responseRawData["data"].(map[string]interface{})
610+
611+
log.Info(userRawData)
612+
// Twitter API does not return E-Mail adresses by default. For that case special privileges have
613+
// to be granted on a per-App basis. See https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/get-account-verify_credentials
614+
615+
// Currently Twitter API only provides the full name of a user. To fill givenName and familyName
616+
// the full name will be split at the first whitespace. This approach will not be valid for all name combinations
617+
nameArr := strings.SplitAfterN(userRawData["name"].(string), " ", 2)
618+
619+
firstName := nameArr[0]
620+
lastName := ""
621+
if len(nameArr) == 2 {
622+
lastName = nameArr[1]
623+
}
624+
nickname := userRawData["username"].(string)
625+
profilePicture := userRawData["profile_image_url"].(string)
626+
627+
user = models.User{
628+
GivenName: &firstName,
629+
FamilyName: &lastName,
630+
Picture: &profilePicture,
631+
Nickname: &nickname,
632+
}
633+
573634
return user, nil
574635
}

server/handlers/oauth_login.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/authorizerdev/authorizer/server/memorystore"
1313
"github.com/authorizerdev/authorizer/server/oauth"
1414
"github.com/authorizerdev/authorizer/server/parsers"
15+
"github.com/authorizerdev/authorizer/server/utils"
1516
"github.com/authorizerdev/authorizer/server/validators"
1617
)
1718

@@ -175,7 +176,10 @@ func OAuthLoginHandler() gin.HandlerFunc {
175176
isProviderConfigured = false
176177
break
177178
}
178-
err := memorystore.Provider.SetState(oauthStateString, constants.AuthRecipeMethodTwitter)
179+
180+
verifier, challenge := utils.GenerateCodeChallenge()
181+
182+
err := memorystore.Provider.SetState(oauthStateString, verifier)
179183
if err != nil {
180184
log.Debug("Error setting state: ", err)
181185
c.JSON(500, gin.H{
@@ -184,7 +188,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
184188
return
185189
}
186190
oauth.OAuthProviders.TwitterConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodTwitter
187-
url := oauth.OAuthProviders.TwitterConfig.AuthCodeURL(oauthStateString)
191+
url := oauth.OAuthProviders.TwitterConfig.AuthCodeURL(oauthStateString, oauth2.SetAuthURLParam("code_challenge", challenge), oauth2.SetAuthURLParam("code_challenge_method", "S256"))
188192
c.Redirect(http.StatusTemporaryRedirect, url)
189193
case constants.AuthRecipeMethodApple:
190194
if oauth.OAuthProviders.AppleConfig == nil {

server/oauth/oauth.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,28 @@ func InitOAuth() error {
134134
}
135135
}
136136

137-
// TODO add support for twitter provider and update OAuthProviders.TwitterConfig
137+
twitterClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwitterClientID)
138+
if err != nil {
139+
twitterClientID = ""
140+
}
141+
twitterClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwitterClientSecret)
142+
if err != nil {
143+
twitterClientSecret = ""
144+
}
145+
if twitterClientID != "" && twitterClientSecret != "" {
146+
OAuthProviders.TwitterConfig = &oauth2.Config{
147+
ClientID: twitterClientID,
148+
ClientSecret: twitterClientSecret,
149+
RedirectURL: "/oauth_callback/twitter",
150+
Endpoint: oauth2.Endpoint{
151+
// Endpoint is currently not yet part of oauth2-package. See https://go-review.googlesource.com/c/oauth2/+/350889 for status
152+
AuthURL: "https://twitter.com/i/oauth2/authorize",
153+
TokenURL: "https://api.twitter.com/2/oauth2/token",
154+
AuthStyle: oauth2.AuthStyleInHeader,
155+
},
156+
Scopes: []string{"tweet.read", "users.read"},
157+
}
158+
}
138159

139160
return nil
140161
}

server/utils/pkce.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package utils
2+
3+
import (
4+
"crypto/sha256"
5+
b64 "encoding/base64"
6+
"math/rand"
7+
"strings"
8+
"time"
9+
)
10+
11+
const (
12+
length = 32
13+
)
14+
15+
// GenerateCodeChallenge creates PKCE-Code-Challenge
16+
// and returns the verifier and challenge
17+
func GenerateCodeChallenge() (string, string) {
18+
// Generate Verifier
19+
randGenerator := rand.New(rand.NewSource(time.Now().UnixNano()))
20+
randomBytes := make([]byte, length)
21+
for i := 0; i < length; i++ {
22+
randomBytes[i] = byte(randGenerator.Intn(255))
23+
}
24+
verifier := strings.Trim(b64.URLEncoding.EncodeToString(randomBytes), "=")
25+
26+
// Generate Challenge
27+
rawChallenge := sha256.New()
28+
rawChallenge.Write([]byte(verifier))
29+
challenge := strings.Trim(b64.URLEncoding.EncodeToString(rawChallenge.Sum(nil)), "=")
30+
31+
return verifier, challenge
32+
}

0 commit comments

Comments
 (0)