Skip to content

Commit

Permalink
Support for VK.com
Browse files Browse the repository at this point in the history
  • Loading branch information
rkashapov authored and markbates committed Jan 26, 2018
1 parent 1f7039e commit 7bce57b
Show file tree
Hide file tree
Showing 6 changed files with 369 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ $ go get github.com/markbates/goth
* Twitch
* Twitter
* Uber
* VK
* Wepay
* Xero
* Yahoo
Expand Down
3 changes: 3 additions & 0 deletions examples/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import (
"github.com/markbates/goth/providers/twitch"
"github.com/markbates/goth/providers/twitter"
"github.com/markbates/goth/providers/uber"
"github.com/markbates/goth/providers/vk"
"github.com/markbates/goth/providers/wepay"
"github.com/markbates/goth/providers/xero"
"github.com/markbates/goth/providers/yahoo"
Expand Down Expand Up @@ -106,6 +107,7 @@ func main() {
//Auth0 allocates domain per customer, a domain must be provided for auth0 to work
auth0.New(os.Getenv("AUTH0_KEY"), os.Getenv("AUTH0_SECRET"), "http://localhost:3000/auth/auth0/callback", os.Getenv("AUTH0_DOMAIN")),
xero.New(os.Getenv("XERO_KEY"), os.Getenv("XERO_SECRET"), "http://localhost:3000/auth/xero/callback"),
vk.New(os.Getenv("VK_KEY"), os.Getenv("VK_SECRET"), "http://localhost:3000/auth/vk/callback"),
)

// OpenID Connect is based on OpenID Connect Auto Discovery URL (https://openid.net/specs/openid-connect-discovery-1_0-17.html)
Expand Down Expand Up @@ -157,6 +159,7 @@ func main() {
m["auth0"] = "Auth0"
m["openid-connect"] = "OpenID Connect"
m["xero"] = "Xero"
m["vk"] = "VK"

var keys []string
for k := range m {
Expand Down
69 changes: 69 additions & 0 deletions providers/vk/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package vk

import (
"encoding/json"
"errors"
"strings"
"time"

"github.com/markbates/goth"
)

// Session stores data during the auth process with VK.
type Session struct {
AuthURL string
AccessToken string
ExpiresAt time.Time
userID int64
email string
}

// GetAuthURL returns the URL for the authentication end-point for the provider.
func (s *Session) GetAuthURL() (string, error) {
if s.AuthURL == "" {
return "", errors.New(goth.NoAuthUrlErrorMessage)
}
return s.AuthURL, nil
}

// Marshal the session into a string
func (s *Session) Marshal() string {
b, _ := json.Marshal(s)
return string(b)
}

// Authorize the session with VK and return the access token to be stored for future use.
func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) {
p := provider.(*Provider)
token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code"))
if err != nil {
return "", err
}

if !token.Valid() {
return "", errors.New("Invalid token received from provider")
}

email, ok := token.Extra("email").(string)
if !ok {
return "", errors.New("Cannot fetch user email")
}

userID, ok := token.Extra("user_id").(float64)
if !ok {
return "", errors.New("Cannot fetch user ID")
}

s.AccessToken = token.AccessToken
s.ExpiresAt = token.Expiry
s.email = email
s.userID = int64(userID)
return s.AccessToken, err
}

// UnmarshalSession will unmarshal a JSON string into a session.
func (p *Provider) UnmarshalSession(data string) (goth.Session, error) {
sess := new(Session)
err := json.NewDecoder(strings.NewReader(data)).Decode(&sess)
return sess, err
}
40 changes: 40 additions & 0 deletions providers/vk/session_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package vk_test

import (
"testing"

"github.com/markbates/goth"
"github.com/markbates/goth/providers/vk"
"github.com/stretchr/testify/assert"
)

func Test_Implements_Session(t *testing.T) {
t.Parallel()
a := assert.New(t)
s := &vk.Session{}

a.Implements((*goth.Session)(nil), s)
}

func Test_GetAuthURL(t *testing.T) {
t.Parallel()
a := assert.New(t)
s := &vk.Session{}

_, err := s.GetAuthURL()
a.Error(err)

s.AuthURL = "/foo"

url, _ := s.GetAuthURL()
a.Equal(url, "/foo")
}

func Test_ToJSON(t *testing.T) {
t.Parallel()
a := assert.New(t)
s := &vk.Session{}

data := s.Marshal()
a.Equal(data, `{"AuthURL":"","AccessToken":"","ExpiresAt":"0001-01-01T00:00:00Z"}`)
}
180 changes: 180 additions & 0 deletions providers/vk/vk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Package vk implements the OAuth2 protocol for authenticating users through vk.com.
// This package can be used as a reference implementation of an OAuth2 provider for Goth.
package vk

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"

"github.com/markbates/goth"
"golang.org/x/oauth2"
)

var (
authURL = "https://oauth.vk.com/authorize"
tokenURL = "https://oauth.vk.com/access_token"
endpointUser = "https://api.vk.com/method/users.get"
apiVersion = "5.71"
)

// New creates a new VK provider and sets up important connection details.
// You should always call `vk.New` to get a new provider. Never try to
// create one manually.
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
providerName: "vk",
}
p.config = newConfig(p, scopes)
return p
}

// Provider is the implementation of `goth.Provider` for accessing Github.
type Provider struct {
ClientKey string
Secret string
CallbackURL string
HTTPClient *http.Client
config *oauth2.Config
providerName string
version string
}

// Name is the name used to retrieve this provider later.
func (p *Provider) Name() string {
return p.providerName
}

// SetName is to update the name of the provider (needed in case of multiple providers of 1 type)
func (p *Provider) SetName(name string) {
p.providerName = name
}

func (p *Provider) Client() *http.Client {
return goth.HTTPClientWithFallBack(p.HTTPClient)
}

// BeginAuth asks VK for an authentication end-point.
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
url := p.config.AuthCodeURL(state)
session := &Session{
AuthURL: url,
}

return session, nil
}

// FetchUser will go to VK and access basic information about the user.
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
sess := session.(*Session)
user := goth.User{
AccessToken: sess.AccessToken,
Provider: p.Name(),
ExpiresAt: sess.ExpiresAt,
Email: sess.email,
}

if user.AccessToken == "" {
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
}

fields := "photo_200,nickname"
requestURL := fmt.Sprintf("%s?user_ids=%d&fields=%s&access_token=%s&v=%s", endpointUser, sess.userID, fields, sess.AccessToken, apiVersion)
response, err := p.Client().Get(requestURL)
if err != nil {
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)
}

bits, err := ioutil.ReadAll(response.Body)

err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData)
if err != nil {
return user, err
}

err = userFromReader(bytes.NewReader(bits), &user)
return user, err
}

func userFromReader(reader io.Reader, user *goth.User) error {
response := struct {
Response []struct {
ID int `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
NickName string `json:"nickname"`
Photo200 string `json:"photo_200"`
} `json:"response"`
}{}

err := json.NewDecoder(reader).Decode(&response)
if err != nil {
return err
}

if len(response.Response) == 0 {
return fmt.Errorf("vk cannot get user information")
}

u := response.Response[0]

user.UserID = string(u.ID)
user.FirstName = u.FirstName
user.LastName = u.LastName
user.NickName = u.NickName
user.AvatarURL = u.Photo200

return err
}

// Debug is a no-op for the vk package.
func (p *Provider) Debug(debug bool) {}

//RefreshToken refresh token is not provided by vk
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
return nil, errors.New("Refresh token is not provided by vk")
}

//RefreshTokenAvailable refresh token is not provided by vk
func (p *Provider) RefreshTokenAvailable() bool {
return false
}

func newConfig(provider *Provider, scopes []string) *oauth2.Config {
c := &oauth2.Config{
ClientID: provider.ClientKey,
ClientSecret: provider.Secret,
RedirectURL: provider.CallbackURL,
Endpoint: oauth2.Endpoint{
AuthURL: authURL,
TokenURL: tokenURL,
},
Scopes: []string{
"email",
},
}

defaultScopes := map[string]struct{}{
"email": {},
}

for _, scope := range scopes {
if _, exists := defaultScopes[scope]; !exists {
c.Scopes = append(c.Scopes, scope)
}
}

return c
}
Loading

0 comments on commit 7bce57b

Please sign in to comment.