Skip to content

Commit 9f7a761

Browse files
authored
Merge pull request #245 from bentranter/feature-add-google-provider
Add simple Google provider
2 parents c614f82 + d18d22e commit 9f7a761

File tree

8 files changed

+387
-2
lines changed

8 files changed

+387
-2
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ $ go get github.com/markbates/goth
3333
* Fitbit
3434
* GitHub
3535
* Gitlab
36-
* Google+
36+
* Google
37+
* Google+ (deprecated)
3738
* Heroku
3839
* InfluxCloud
3940
* Instagram

examples/main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/markbates/goth/providers/fitbit"
3030
"github.com/markbates/goth/providers/github"
3131
"github.com/markbates/goth/providers/gitlab"
32+
"github.com/markbates/goth/providers/google"
3233
"github.com/markbates/goth/providers/gplus"
3334
"github.com/markbates/goth/providers/heroku"
3435
"github.com/markbates/goth/providers/instagram"
@@ -65,6 +66,7 @@ func main() {
6566

6667
facebook.New(os.Getenv("FACEBOOK_KEY"), os.Getenv("FACEBOOK_SECRET"), "http://localhost:3000/auth/facebook/callback"),
6768
fitbit.New(os.Getenv("FITBIT_KEY"), os.Getenv("FITBIT_SECRET"), "http://localhost:3000/auth/fitbit/callback"),
69+
google.New(os.Getenv("GOOGLE_KEY"), os.Getenv("GOOGLE_SECRET"), "http://localhost:3000/auth/google/callback"),
6870
gplus.New(os.Getenv("GPLUS_KEY"), os.Getenv("GPLUS_SECRET"), "http://localhost:3000/auth/gplus/callback"),
6971
github.New(os.Getenv("GITHUB_KEY"), os.Getenv("GITHUB_SECRET"), "http://localhost:3000/auth/github/callback"),
7072
spotify.New(os.Getenv("SPOTIFY_KEY"), os.Getenv("SPOTIFY_SECRET"), "http://localhost:3000/auth/spotify/callback"),
@@ -134,6 +136,8 @@ func main() {
134136
m["fitbit"] = "Fitbit"
135137
m["github"] = "Github"
136138
m["gitlab"] = "Gitlab"
139+
m["google"] = "Google"
140+
m["gplus"] = "Google Plus"
137141
m["soundcloud"] = "SoundCloud"
138142
m["spotify"] = "Spotify"
139143
m["steam"] = "Steam"
@@ -143,7 +147,6 @@ func main() {
143147
m["wepay"] = "Wepay"
144148
m["yahoo"] = "Yahoo"
145149
m["yammer"] = "Yammer"
146-
m["gplus"] = "Google Plus"
147150
m["heroku"] = "Heroku"
148151
m["instagram"] = "Instagram"
149152
m["intercom"] = "Intercom"

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module github.com/markbates/goth
22

33
require (
4+
cloud.google.com/go v0.30.0
45
github.com/gorilla/mux v1.6.2
56
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1
67
github.com/gorilla/sessions v1.1.1

go.sum

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
cloud.google.com/go v0.30.0 h1:xKvyLgk56d0nksWq49J0UyGEeUIicTl4+UBiX1NPX9g=
2+
cloud.google.com/go v0.30.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3+
cloud.google.com/go/compute v0.0.0-20181010175407-5f0ffe772937/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
4+
cloud.google.com/go/compute/metadata v0.0.0-20181010175407-5f0ffe772937/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
5+
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
6+
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
7+
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
8+
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
9+
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1 h1:LqbZZ9sNMWVjeXS4NN5oVvhMjDyLhmA1LG86oSo+IqY=
10+
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
11+
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
12+
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
13+
github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE=
14+
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
15+
github.com/markbates/going v1.0.0 h1:DQw0ZP7NbNlFGcKbcE/IVSOAFzScxRtLpd0rLMzLhq0=
16+
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
17+
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c h1:3wkDRdxK92dF+c1ke2dtj7ZzemFWBHB9plnJOtlwdFA=
18+
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
19+
golang.org/x/net v0.0.0-20180706051357-32a936f46389 h1:U+zCn5sqaq+q4hrnMrz9sgrW1yatwEOUgYkGt3u9ZOU=
20+
golang.org/x/net v0.0.0-20180706051357-32a936f46389/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
21+
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd h1:QQhib242ErYDSMitlBm8V7wYCm/1a25hV8qMadIKLPA=
22+
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=

providers/google/google.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Package google implements the OAuth2 protocol for authenticating users
2+
// through Google.
3+
package google
4+
5+
import (
6+
"encoding/json"
7+
"net/http"
8+
"net/url"
9+
"strings"
10+
11+
"fmt"
12+
13+
"github.com/markbates/goth"
14+
"golang.org/x/oauth2"
15+
goog "golang.org/x/oauth2/google"
16+
)
17+
18+
const endpointProfile string = "https://www.googleapis.com/oauth2/v2/userinfo"
19+
20+
// New creates a new Google provider, and sets up important connection details.
21+
// You should always call `google.New` to get a new Provider. Never try to create
22+
// one manually.
23+
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
24+
p := &Provider{
25+
ClientKey: clientKey,
26+
Secret: secret,
27+
CallbackURL: callbackURL,
28+
providerName: "google",
29+
}
30+
p.config = newConfig(p, scopes)
31+
return p
32+
}
33+
34+
// Provider is the implementation of `goth.Provider` for accessing Google.
35+
type Provider struct {
36+
ClientKey string
37+
Secret string
38+
CallbackURL string
39+
HTTPClient *http.Client
40+
config *oauth2.Config
41+
prompt oauth2.AuthCodeOption
42+
providerName string
43+
}
44+
45+
// Name is the name used to retrieve this provider later.
46+
func (p *Provider) Name() string {
47+
return p.providerName
48+
}
49+
50+
// SetName is to update the name of the provider (needed in case of multiple providers of 1 type)
51+
func (p *Provider) SetName(name string) {
52+
p.providerName = name
53+
}
54+
55+
// Client returns an HTTP client to be used in all fetch operations.
56+
func (p *Provider) Client() *http.Client {
57+
return goth.HTTPClientWithFallBack(p.HTTPClient)
58+
}
59+
60+
// Debug is a no-op for the google package.
61+
func (p *Provider) Debug(debug bool) {}
62+
63+
// BeginAuth asks Google for an authentication endpoint.
64+
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
65+
var opts []oauth2.AuthCodeOption
66+
if p.prompt != nil {
67+
opts = append(opts, p.prompt)
68+
}
69+
url := p.config.AuthCodeURL(state, opts...)
70+
session := &Session{
71+
AuthURL: url,
72+
}
73+
return session, nil
74+
}
75+
76+
type googleUser struct {
77+
ID string `json:"id"`
78+
Email string `json:"email"`
79+
Name string `json:"name"`
80+
FirstName string `json:"given_name"`
81+
LastName string `json:"family_name"`
82+
Link string `json:"link"`
83+
Picture string `json:"picture"`
84+
}
85+
86+
// FetchUser will go to Google and access basic information about the user.
87+
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
88+
sess := session.(*Session)
89+
user := goth.User{
90+
AccessToken: sess.AccessToken,
91+
Provider: p.Name(),
92+
RefreshToken: sess.RefreshToken,
93+
ExpiresAt: sess.ExpiresAt,
94+
}
95+
96+
if user.AccessToken == "" {
97+
// Data is not yet retrieved, since accessToken is still empty.
98+
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
99+
}
100+
101+
response, err := p.Client().Get(endpointProfile + "?access_token=" + url.QueryEscape(sess.AccessToken))
102+
if err != nil {
103+
return user, err
104+
}
105+
if response.StatusCode != http.StatusOK {
106+
return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
107+
}
108+
109+
u := &googleUser{}
110+
if err := json.NewDecoder(response.Body).Decode(u); err != nil {
111+
return user, err
112+
}
113+
defer response.Body.Close()
114+
115+
// Extract the user data we got from Google into our goth.User.
116+
user.Name = u.Name
117+
user.FirstName = u.FirstName
118+
user.LastName = u.LastName
119+
user.NickName = u.Name
120+
user.Email = u.Email
121+
user.AvatarURL = u.Picture
122+
user.UserID = u.ID
123+
124+
return user, nil
125+
}
126+
127+
func newConfig(provider *Provider, scopes []string) *oauth2.Config {
128+
c := &oauth2.Config{
129+
ClientID: provider.ClientKey,
130+
ClientSecret: provider.Secret,
131+
RedirectURL: provider.CallbackURL,
132+
Endpoint: goog.Endpoint,
133+
Scopes: []string{},
134+
}
135+
136+
if len(scopes) > 0 {
137+
for _, scope := range scopes {
138+
c.Scopes = append(c.Scopes, scope)
139+
}
140+
} else {
141+
c.Scopes = []string{"email"}
142+
}
143+
return c
144+
}
145+
146+
//RefreshTokenAvailable refresh token is provided by auth provider or not
147+
func (p *Provider) RefreshTokenAvailable() bool {
148+
return true
149+
}
150+
151+
//RefreshToken get new access token based on the refresh token
152+
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
153+
token := &oauth2.Token{RefreshToken: refreshToken}
154+
ts := p.config.TokenSource(goth.ContextForClient(p.Client()), token)
155+
newToken, err := ts.Token()
156+
if err != nil {
157+
return nil, err
158+
}
159+
return newToken, err
160+
}
161+
162+
// SetPrompt sets the prompt values for the google OAuth call. Use this to
163+
// force users to choose and account every time by passing "select_account",
164+
// for example.
165+
// See https://developers.google.com/identity/protocols/OpenIDConnect#authenticationuriparameters
166+
func (p *Provider) SetPrompt(prompt ...string) {
167+
if len(prompt) == 0 {
168+
return
169+
}
170+
p.prompt = oauth2.SetAuthURLParam("prompt", strings.Join(prompt, " "))
171+
}

providers/google/google_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package google_test
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"testing"
7+
8+
"github.com/markbates/goth"
9+
"github.com/markbates/goth/providers/google"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func Test_New(t *testing.T) {
14+
t.Parallel()
15+
a := assert.New(t)
16+
17+
provider := googleProvider()
18+
a.Equal(provider.ClientKey, os.Getenv("GOOGLE_KEY"))
19+
a.Equal(provider.Secret, os.Getenv("GOOGLE_SECRET"))
20+
a.Equal(provider.CallbackURL, "/foo")
21+
}
22+
23+
func Test_BeginAuth(t *testing.T) {
24+
t.Parallel()
25+
a := assert.New(t)
26+
27+
provider := googleProvider()
28+
session, err := provider.BeginAuth("test_state")
29+
s := session.(*google.Session)
30+
a.NoError(err)
31+
a.Contains(s.AuthURL, "accounts.google.com/o/oauth2/auth")
32+
a.Contains(s.AuthURL, fmt.Sprintf("client_id=%s", os.Getenv("GOOGLE_KEY")))
33+
a.Contains(s.AuthURL, "state=test_state")
34+
a.Contains(s.AuthURL, "scope=email")
35+
}
36+
37+
func Test_BeginAuthWithPrompt(t *testing.T) {
38+
// This exists because there was a panic caused by the oauth2 package when
39+
// the AuthCodeOption passed was nil. This test uses it, Test_BeginAuth does
40+
// not, to ensure both cases are covered.
41+
t.Parallel()
42+
a := assert.New(t)
43+
44+
provider := googleProvider()
45+
provider.SetPrompt("test", "prompts")
46+
session, err := provider.BeginAuth("test_state")
47+
s := session.(*google.Session)
48+
a.NoError(err)
49+
a.Contains(s.AuthURL, "accounts.google.com/o/oauth2/auth")
50+
a.Contains(s.AuthURL, fmt.Sprintf("client_id=%s", os.Getenv("GOOGLE_KEY")))
51+
a.Contains(s.AuthURL, "state=test_state")
52+
a.Contains(s.AuthURL, "scope=email")
53+
a.Contains(s.AuthURL, "prompt=test+prompts")
54+
}
55+
56+
func Test_Implements_Provider(t *testing.T) {
57+
t.Parallel()
58+
a := assert.New(t)
59+
60+
a.Implements((*goth.Provider)(nil), googleProvider())
61+
}
62+
63+
func Test_SessionFromJSON(t *testing.T) {
64+
t.Parallel()
65+
a := assert.New(t)
66+
67+
provider := googleProvider()
68+
69+
s, err := provider.UnmarshalSession(`{"AuthURL":"https://accounts.google.com/o/oauth2/auth","AccessToken":"1234567890"}`)
70+
a.NoError(err)
71+
session := s.(*google.Session)
72+
a.Equal(session.AuthURL, "https://accounts.google.com/o/oauth2/auth")
73+
a.Equal(session.AccessToken, "1234567890")
74+
}
75+
76+
func googleProvider() *google.Provider {
77+
return google.New(os.Getenv("GOOGLE_KEY"), os.Getenv("GOOGEL_SECRET"), "/foo")
78+
}

providers/google/session.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package google
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"strings"
7+
"time"
8+
9+
"github.com/markbates/goth"
10+
)
11+
12+
// Session stores data during the auth process with Google.
13+
type Session struct {
14+
AuthURL string
15+
AccessToken string
16+
RefreshToken string
17+
ExpiresAt time.Time
18+
}
19+
20+
// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Google provider.
21+
func (s Session) GetAuthURL() (string, error) {
22+
if s.AuthURL == "" {
23+
return "", errors.New(goth.NoAuthUrlErrorMessage)
24+
}
25+
return s.AuthURL, nil
26+
}
27+
28+
// Authorize the session with Google and return the access token to be stored for future use.
29+
func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) {
30+
p := provider.(*Provider)
31+
token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code"))
32+
if err != nil {
33+
return "", err
34+
}
35+
36+
if !token.Valid() {
37+
return "", errors.New("Invalid token received from provider")
38+
}
39+
40+
s.AccessToken = token.AccessToken
41+
s.RefreshToken = token.RefreshToken
42+
s.ExpiresAt = token.Expiry
43+
return token.AccessToken, err
44+
}
45+
46+
// Marshal the session into a string
47+
func (s Session) Marshal() string {
48+
b, _ := json.Marshal(s)
49+
return string(b)
50+
}
51+
52+
func (s Session) String() string {
53+
return s.Marshal()
54+
}
55+
56+
// UnmarshalSession will unmarshal a JSON string into a session.
57+
func (p *Provider) UnmarshalSession(data string) (goth.Session, error) {
58+
sess := &Session{}
59+
err := json.NewDecoder(strings.NewReader(data)).Decode(sess)
60+
return sess, err
61+
}

0 commit comments

Comments
 (0)