-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathauth_google.go
214 lines (180 loc) · 6.62 KB
/
auth_google.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
package titan
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/neptulon/neptulon"
"github.com/neptulon/neptulon/middleware"
"github.com/titan-x/titan/data"
"github.com/titan-x/titan/models"
)
type gProfile struct {
Name string
Email string
Picture []byte
}
type tokenContainer struct {
Token string `json:"token"`
}
type gAuthRes struct {
Token string `json:"token"`
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Picture []byte `json:"picture"`
}
// googleAuth authenticates a user with Google+ using provided OAuth 2.0 access token.
// If authenticated successfully, user profile is retrieved from Google+ and user is given a JWT token in return.
func googleAuth(ctx *neptulon.ReqCtx, db data.DB, pass string) error {
var r tokenContainer
if err := ctx.Params(&r); err != nil || r.Token == "" {
ctx.Err = &neptulon.ResError{Code: 666, Message: "Malformed or null Google oauth access token was provided."}
return fmt.Errorf("auth: google: malformed or null Google oauth token '%v' was provided: %v", r.Token, err)
}
p, err := getTokenInfo(r.Token)
if err != nil {
ctx.Err = &neptulon.ResError{Code: 666, Message: "Failed to authenticated with the given Google oauth access token."}
return fmt.Errorf("auth: google: error during Google API call using provided token: %v with error: %v", r.Token, err)
}
// retrieve user information
user, ok := db.GetByEmail(p.Email)
if !ok {
// this is a first-time registration so create user profile via Google+ profile info
user = &models.User{Email: p.Email, Name: p.Name, Picture: p.Picture, Registered: time.Now()}
// save the user information for user ID to be generated by the database
if ierr := db.SaveUser(user); ierr != nil {
return fmt.Errorf("auth: google: failed to persist user information: %v", ierr)
}
// create the JWT token
token := jwt.New(jwt.SigningMethodHS256)
token.Claims["userid"] = user.ID
token.Claims["created"] = user.Registered.Unix()
user.JWTToken, err = token.SignedString([]byte(pass))
if err != nil {
return fmt.Errorf("auth: google: jwt signing error: %v", err)
}
// now save the full user info
if err := db.SaveUser(user); err != nil {
return fmt.Errorf("auth: google: failed to persist user information: %v", err)
}
// store user ID in session so user can make authenticated call after this
ctx.Conn.Session.Set("userid", user.ID)
}
ctx.Res = gAuthRes{ID: user.ID, Token: user.JWTToken, Name: user.Name, Email: user.Email, Picture: user.Picture}
ctx.Session.Set(middleware.CustResLogDataKey, gAuthRes{ID: user.ID, Token: user.JWTToken, Name: user.Name, Email: user.Email})
log.Printf("auth: google: logged in: %v, %v", p.Name, p.Email)
return nil
}
// ################ Google OAuth2 TokenInfo API Call ################
// verifyIDToken verifies and returns ID token info as described in:
// https://developers.google.com/identity/sign-in/android/backend-auth#send-the-id-token-to-your-server
func getTokenInfo(idToken string) (profile *gProfile, err error) {
uri := fmt.Sprintf("https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=%s", idToken)
res, err := http.Get(uri)
if err != nil || res.StatusCode >= http.StatusBadRequest {
err = fmt.Errorf("failed to call google oauth2 api with error: %v, and response: %+v", err, res)
return
}
resBody, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
err = fmt.Errorf("failed to read response from google oauth2 api with error: %v", err)
return
}
var ti gTokenInfo
if err = json.Unmarshal(resBody, &ti); err != nil {
err = fmt.Errorf("failed to deserialize google oauth2 api response with error: %v", err)
return
}
// check that 'aud' claim contains our client id
if ti.AUD != gServerClient {
err = fmt.Errorf("given google oauth2 id token belongs to another app id: %v", ti.AUD)
return
}
// retrieve profile image
uri = ti.Picture
res, err = http.Get(uri)
if err != nil {
err = fmt.Errorf("failed to call google oauth2 api to get user image with error: %v", err)
return
}
profilePic, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
err = fmt.Errorf("failed to read google oauth2 api user profile pic response with error: %v", err)
return
}
profile = &gProfile{Name: ti.GivenName + " " + ti.FamilyName, Email: ti.Email, Picture: profilePic}
return
}
var gServerClient = "218602439235-6g09g0ap6i8v25v3rel49rtqjcu9ppj0.apps.googleusercontent.com"
type gTokenInfo struct {
ISS string
SUB string
AZP string
AUD string
IAT string
EXP string
Email string
EmailVerified string `json:"email_verified"`
Name string
Picture string // profile pic URL
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Locale string
}
// ################ Google+ API Call ################
// getGPProfile retrieves user info (display name, e-mail, profile pic) using an oauth2 access token that has 'profile' and 'email' scopes.
// Also retrieves user profile image via profile image URL provided the response.
func getGPProfile(oauthToken string) (profile *gProfile, err error) {
// retrieve profile info from Google
uri := fmt.Sprintf("https://www.googleapis.com/plus/v1/people/me?access_token=%s", oauthToken)
res, err := http.Get(uri)
if err != nil || res.StatusCode >= http.StatusBadRequest {
err = fmt.Errorf("failed to call google+ api with error: %v, and response: %+v", err, res)
return
}
resBody, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
err = fmt.Errorf("failed to read response from google+ api with error: %v", err)
return
}
var p gPlusProfile
if err = json.Unmarshal(resBody, &p); err != nil {
err = fmt.Errorf("failed to deserialize google+ api response with error: %v", err)
return
}
// retrieve profile image
uri = p.Image.URL
res, err = http.Get(uri)
if err != nil {
err = fmt.Errorf("failed to call google+ api to get user image with error: %v", err)
return
}
profilePic, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
err = fmt.Errorf("failed to read google+ api user profile pic response with error: %v", err)
return
}
profile = &gProfile{Name: p.DisplayName, Email: p.Emails[0].Value, Picture: profilePic}
return
}
// Response from GET https://www.googleapis.com/plus/v1/people/me?access_token=... (with scope 'profile' and 'email')
// has the following structure with denoted fields of interest (rest is left out):
type gPlusProfile struct {
Emails []gPlusEmail
DisplayName string
Image gPlusImage
}
type gPlusEmail struct {
Value string
}
type gPlusImage struct {
URL string
}