If the user chooses to disconnect, the app must delete all stored
58 | information retrieved from Google for the given user.
59 |
60 |
61 |
User's profile information
62 |
This data is retrieved client-side by using the Google JavaScript API
63 | client library.
64 |
65 |
66 |
User's friends that are visible to this app
67 |
This data is retrieved from your server, where your server makes
68 | an authorized HTTP request on the user's behalf.
69 |
If your app uses server-side rendering, this is the section you
70 | would change using your server-side templating system.
71 |
72 |
73 |
Authentication Logs
74 |
75 |
76 |
77 |
290 |
291 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc. All Rights Reserved
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 | // use this file except in compliance with the License. You may obtain a copy of
5 | // the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | // License for the specific language governing permissions and limitations under
13 | // the License.
14 |
15 | // Package main provides a simple server to demonstrate how to use Google+
16 | // Sign-In and make a request via your own server.
17 | package main
18 |
19 | import (
20 | "crypto/rand"
21 | "encoding/base64"
22 | "encoding/json"
23 | "errors"
24 | "fmt"
25 | "html/template"
26 | "io/ioutil"
27 | "log"
28 | "net/http"
29 | "net/url"
30 | "strings"
31 |
32 | "google.golang.org/api/plus/v1"
33 | "github.com/gorilla/sessions"
34 | "golang.org/x/oauth2"
35 | "golang.org/x/oauth2/google"
36 | )
37 |
38 | // Update your Google API project information here.
39 | const (
40 | clientID = "YOUR_CLIENT_ID"
41 | clientSecret = "YOUR_CLIENT_SECRET"
42 | applicationName = "Google+ Go Quickstart"
43 | )
44 |
45 | // config is the configuration specification supplied to the OAuth package.
46 | var config = &oauth2.Config{
47 | ClientID: clientID,
48 | ClientSecret: clientSecret,
49 | // Scope determines which API calls you are authorized to make
50 | Scopes: []string{"https://www.googleapis.com/auth/plus.login"},
51 | Endpoint: google.Endpoint,
52 | // Use "postmessage" for the code-flow for server side apps
53 | RedirectURL: "postmessage",
54 | }
55 |
56 | // store initializes the Gorilla session store.
57 | var store = sessions.NewCookieStore([]byte(randomString(32)))
58 |
59 | // indexTemplate is the HTML template we use to present the index page.
60 | var indexTemplate = template.Must(template.ParseFiles("index.html"))
61 |
62 | // Token represents an OAuth token response.
63 | type Token struct {
64 | AccessToken string `json:"access_token"`
65 | TokenType string `json:"token_type"`
66 | ExpiresIn int `json:"expires_in"`
67 | IdToken string `json:"id_token"`
68 | }
69 |
70 | // ClaimSet represents an IdToken response.
71 | type ClaimSet struct {
72 | Sub string
73 | }
74 |
75 | // exchange takes an authentication code and exchanges it with the OAuth
76 | // endpoint for a Google API bearer token and a Google+ ID
77 | func exchange(code string) (accessToken string, idToken string, err error) {
78 | tok, err := config.Exchange(oauth2.NoContext, code)
79 | if err != nil {
80 | return "", "", fmt.Errorf("Error while exchanging code: %v", err)
81 | }
82 | // TODO: return ID token in second parameter from updated oauth2 interface
83 | return tok.AccessToken, tok.Extra("id_token").(string), nil
84 | }
85 |
86 | // decodeIdToken takes an ID Token and decodes it to fetch the Google+ ID within
87 | func decodeIdToken(idToken string) (gplusID string, err error) {
88 | // An ID token is a cryptographically-signed JSON object encoded in base 64.
89 | // Normally, it is critical that you validate an ID token before you use it,
90 | // but since you are communicating directly with Google over an
91 | // intermediary-free HTTPS channel and using your Client Secret to
92 | // authenticate yourself to Google, you can be confident that the token you
93 | // receive really comes from Google and is valid. If your server passes the ID
94 | // token to other components of your app, it is extremely important that the
95 | // other components validate the token before using it.
96 | var set ClaimSet
97 | if idToken != "" {
98 | // Check that the padding is correct for a base64decode
99 | parts := strings.Split(idToken, ".")
100 | if len(parts) < 2 {
101 | return "", fmt.Errorf("Malformed ID token")
102 | }
103 | // Decode the ID token
104 | b, err := base64Decode(parts[1])
105 | if err != nil {
106 | return "", fmt.Errorf("Malformed ID token: %v", err)
107 | }
108 | err = json.Unmarshal(b, &set)
109 | if err != nil {
110 | return "", fmt.Errorf("Malformed ID token: %v", err)
111 | }
112 | }
113 | return set.Sub, nil
114 | }
115 |
116 | // index sets up a session for the current user and serves the index page
117 | func index(w http.ResponseWriter, r *http.Request) *appError {
118 | // This check prevents the "/" handler from handling all requests by default
119 | if r.URL.Path != "/" {
120 | http.NotFound(w, r)
121 | return nil
122 | }
123 |
124 | // Create a state token to prevent request forgery and store it in the session
125 | // for later validation
126 | session, err := store.Get(r, "sessionName")
127 | if err != nil {
128 | log.Println("error fetching session:", err)
129 | // Ignore the initial session fetch error, as Get() always returns a
130 | // session, even if empty.
131 | //return &appError{err, "Error fetching session", 500}
132 | }
133 | state := randomString(64)
134 | session.Values["state"] = state
135 | session.Save(r, w)
136 |
137 | stateURL := url.QueryEscape(session.Values["state"].(string))
138 |
139 | // Fill in the missing fields in index.html
140 | var data = struct {
141 | ApplicationName, ClientID, State string
142 | }{applicationName, clientID, stateURL}
143 |
144 | // Render and serve the HTML
145 | err = indexTemplate.Execute(w, data)
146 | if err != nil {
147 | log.Println("error rendering template:", err)
148 | return &appError{err, "Error rendering template", 500}
149 | }
150 | return nil
151 | }
152 |
153 | // connect exchanges the one-time authorization code for a token and stores the
154 | // token in the session
155 | func connect(w http.ResponseWriter, r *http.Request) *appError {
156 | // Ensure that the request is not a forgery and that the user sending this
157 | // connect request is the expected user
158 | session, err := store.Get(r, "sessionName")
159 | if err != nil {
160 | log.Println("error fetching session:", err)
161 | return &appError{err, "Error fetching session", 500}
162 | }
163 | if r.FormValue("state") != session.Values["state"].(string) {
164 | m := "Invalid state parameter"
165 | return &appError{errors.New(m), m, 401}
166 | }
167 | // Normally, the state is a one-time token; however, in this example, we want
168 | // the user to be able to connect and disconnect without reloading the page.
169 | // Thus, for demonstration, we don't implement this best practice.
170 | // session.Values["state"] = nil
171 |
172 | // Setup for fetching the code from the request payload
173 | x, err := ioutil.ReadAll(r.Body)
174 | if err != nil {
175 | return &appError{err, "Error reading code in request body", 500}
176 | }
177 | code := string(x)
178 |
179 | accessToken, idToken, err := exchange(code)
180 | if err != nil {
181 | return &appError{err, "Error exchanging code for access token", 500}
182 | }
183 | gplusID, err := decodeIdToken(idToken)
184 | if err != nil {
185 | return &appError{err, "Error decoding ID token", 500}
186 | }
187 |
188 | // Check if the user is already connected
189 | storedToken := session.Values["accessToken"]
190 | storedGPlusID := session.Values["gplusID"]
191 | if storedToken != nil && storedGPlusID == gplusID {
192 | m := "Current user already connected"
193 | return &appError{errors.New(m), m, 200}
194 | }
195 |
196 | // Store the access token in the session for later use
197 | session.Values["accessToken"] = accessToken
198 | session.Values["gplusID"] = gplusID
199 | session.Save(r, w)
200 | return nil
201 | }
202 |
203 | // disconnect revokes the current user's token and resets their session
204 | func disconnect(w http.ResponseWriter, r *http.Request) *appError {
205 | // Only disconnect a connected user
206 | session, err := store.Get(r, "sessionName")
207 | if err != nil {
208 | log.Println("error fetching session:", err)
209 | return &appError{err, "Error fetching session", 500}
210 | }
211 | token := session.Values["accessToken"]
212 | if token == nil {
213 | m := "Current user not connected"
214 | return &appError{errors.New(m), m, 401}
215 | }
216 |
217 | // Execute HTTP GET request to revoke current token
218 | url := "https://accounts.google.com/o/oauth2/revoke?token=" + token.(string)
219 | resp, err := http.Get(url)
220 | if err != nil {
221 | m := "Failed to revoke token for a given user"
222 | return &appError{errors.New(m), m, 400}
223 | }
224 | defer resp.Body.Close()
225 |
226 | // Reset the user's session
227 | session.Values["accessToken"] = nil
228 | session.Save(r, w)
229 | return nil
230 | }
231 |
232 | // people fetches the list of people user has shared with this app
233 | func people(w http.ResponseWriter, r *http.Request) *appError {
234 | session, err := store.Get(r, "sessionName")
235 | if err != nil {
236 | log.Println("error fetching session:", err)
237 | return &appError{err, "Error fetching session", 500}
238 | }
239 | token := session.Values["accessToken"]
240 | // Only fetch a list of people for connected users
241 | if token == nil {
242 | m := "Current user not connected"
243 | return &appError{errors.New(m), m, 401}
244 | }
245 |
246 | // Create a new authorized API client
247 | tok := new(oauth2.Token)
248 | tok.AccessToken = token.(string)
249 | client := oauth2.NewClient(oauth2.NoContext, oauth2.StaticTokenSource(tok))
250 | service, err := plus.New(client)
251 | if err != nil {
252 | return &appError{err, "Create Plus Client", 500}
253 | }
254 |
255 | // Get a list of people that this user has shared with this app
256 | people := service.People.List("me", "visible")
257 | peopleFeed, err := people.Do()
258 | if err != nil {
259 | m := "Failed to refresh access token"
260 | if err.Error() == "AccessTokenRefreshError" {
261 | return &appError{errors.New(m), m, 500}
262 | }
263 | return &appError{err, m, 500}
264 | }
265 | w.Header().Set("Content-type", "application/json")
266 | err = json.NewEncoder(w).Encode(&peopleFeed)
267 | if err != nil {
268 | return &appError{err, "Convert PeopleFeed to JSON", 500}
269 | }
270 | return nil
271 | }
272 |
273 | // appHandler is to be used in error handling
274 | type appHandler func(http.ResponseWriter, *http.Request) *appError
275 |
276 | type appError struct {
277 | Err error
278 | Message string
279 | Code int
280 | }
281 |
282 | // serveHTTP formats and passes up an error
283 | func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
284 | if e := fn(w, r); e != nil { // e is *appError, not os.Error.
285 | log.Println(e.Err)
286 | http.Error(w, e.Message, e.Code)
287 | }
288 | }
289 |
290 | // randomString returns a random string with the specified length
291 | func randomString(length int) (str string) {
292 | b := make([]byte, length)
293 | rand.Read(b)
294 | return base64.StdEncoding.EncodeToString(b)
295 | }
296 |
297 | func base64Decode(s string) ([]byte, error) {
298 | // add back missing padding
299 | switch len(s) % 4 {
300 | case 2:
301 | s += "=="
302 | case 3:
303 | s += "="
304 | }
305 | return base64.URLEncoding.DecodeString(s)
306 | }
307 |
308 | func main() {
309 | // Register a handler for our API calls
310 | http.Handle("/connect", appHandler(connect))
311 | http.Handle("/disconnect", appHandler(disconnect))
312 | http.Handle("/people", appHandler(people))
313 |
314 | // Serve the index.html page
315 | http.Handle("/", appHandler(index))
316 | http.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
317 | http.ServeFile(w, r, r.URL.Path[1:])
318 | })
319 | err := http.ListenAndServe(":4567", nil)
320 | if err != nil {
321 | log.Fatal("ListenAndServe: ", err)
322 | }
323 |
324 | }
325 |
--------------------------------------------------------------------------------
/static/signin_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/googlearchive/gplus-quickstart-go/0effdbdc1228c33d50a8db53324298fb887a0906/static/signin_button.png
--------------------------------------------------------------------------------