diff --git a/providers/line/line.go b/providers/line/line.go index e4d2e9555..7f0cf1b7c 100644 --- a/providers/line/line.go +++ b/providers/line/line.go @@ -8,7 +8,9 @@ import ( "fmt" "io/ioutil" "net/http" + "time" + "github.com/golang-jwt/jwt/v4" "github.com/markbates/goth" "golang.org/x/oauth2" ) @@ -17,8 +19,14 @@ const ( authURL string = "https://access.line.me/oauth2/v2.1/authorize" tokenURL string = "https://api.line.me/oauth2/v2.1/token" endpointUser string = "https://api.line.me/v2/profile" + issuerURL string = "https://access.line.me" ) +type IDTokenClaims struct { + jwt.StandardClaims + Email string `json:"email"` +} + // Provider is the implementation of `goth.Provider` for accessing Line.me. type Provider struct { ClientKey string @@ -95,11 +103,9 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { response, err := c.Do(req) if err != nil { - if response != nil { - response.Body.Close() - } return user, err } + defer response.Body.Close() if response.StatusCode != http.StatusOK { return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode) @@ -125,6 +131,13 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { user.NickName = u.DisplayName user.AvatarURL = u.PictureURL user.UserID = u.UserID + + if sess.IDToken != "" { + if err = p.addDataFromIdToken(sess.IDToken, &user); err != nil { + return user, err + } + } + return user, err } @@ -141,9 +154,7 @@ func newConfig(provider *Provider, scopes []string) *oauth2.Config { } if len(scopes) > 0 { - for _, scope := range scopes { - c.Scopes = append(c.Scopes, scope) - } + c.Scopes = append(c.Scopes, scopes...) } return c } @@ -167,3 +178,36 @@ func (p *Provider) SetBotPrompt(botPrompt string) { } p.authCodeOptions = append(p.authCodeOptions, oauth2.SetAuthURLParam("bot_prompt", botPrompt)) } + +func (p *Provider) addDataFromIdToken(idToken string, user *goth.User) error { + token, err := jwt.ParseWithClaims(idToken, &IDTokenClaims{}, func(t *jwt.Token) (interface{}, error) { + claims := t.Claims.(*IDTokenClaims) + vErr := new(jwt.ValidationError) + + if !claims.VerifyAudience(p.ClientKey, true) { + vErr.Inner = fmt.Errorf("audience is incorrect") + vErr.Errors |= jwt.ValidationErrorAudience + } + if !claims.VerifyIssuer(issuerURL, true) { + vErr.Inner = fmt.Errorf("issuer is incorrect") + vErr.Errors |= jwt.ValidationErrorIssuer + } + if !claims.VerifyExpiresAt(time.Now().Unix(), true) { + vErr.Inner = fmt.Errorf("token is expired") + vErr.Errors |= jwt.ValidationErrorExpired + } + if vErr.Errors > 0 { + return nil, vErr + } + + return []byte(p.Secret), nil + }) + + if err != nil { + return err + } + + user.Email = token.Claims.(*IDTokenClaims).Email + + return nil +} diff --git a/providers/line/session.go b/providers/line/session.go index 158a2dde1..0213bc15f 100644 --- a/providers/line/session.go +++ b/providers/line/session.go @@ -15,6 +15,7 @@ type Session struct { AccessToken string RefreshToken string ExpiresAt time.Time + IDToken string } var _ goth.Session = &Session{} @@ -42,6 +43,11 @@ func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, s.AccessToken = token.AccessToken s.RefreshToken = token.RefreshToken s.ExpiresAt = token.Expiry + + if idToken := token.Extra("id_token"); idToken != nil { + s.IDToken = idToken.(string) + } + return token.AccessToken, err } diff --git a/providers/line/session_test.go b/providers/line/session_test.go index 719bbb1e3..827565d42 100644 --- a/providers/line/session_test.go +++ b/providers/line/session_test.go @@ -36,7 +36,7 @@ func Test_ToJSON(t *testing.T) { s := &line.Session{} data := s.Marshal() - a.Equal(data, `{"AuthURL":"","AccessToken":"","RefreshToken":"","ExpiresAt":"0001-01-01T00:00:00Z"}`) + a.Equal(data, `{"AuthURL":"","AccessToken":"","RefreshToken":"","ExpiresAt":"0001-01-01T00:00:00Z","IDToken":""}`) } func Test_String(t *testing.T) {