├── README.md ├── index.html ├── main.go └── static └── signin_button.png /README.md: -------------------------------------------------------------------------------- 1 | # Google+ Go Quick-Start 2 | 3 | The documentation for this quick-start is maintained on developers.google.com. 4 | Please see here for more information: 5 | https://developers.google.com/+/quickstart/go 6 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | {{ .ApplicationName }} 25 | 36 | 38 | 39 | 40 | 50 | 51 |
52 | Sign in with Google+ 54 |
55 | 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 --------------------------------------------------------------------------------