├── .gitignore ├── LICENSE ├── README.md ├── appengine_openid ├── appengine_openid.go └── appengine_openid_test.go ├── auth.go ├── auth_test.go ├── dev ├── dev.go └── dev_test.go ├── facebook └── facebook.go ├── github └── github.go ├── google ├── google.go └── google_test.go ├── oauth2 ├── oauth2.go └── oauth2_test.go ├── password ├── password.go ├── password_test.go ├── provider.go ├── provider_test.go ├── service.go └── service_test.go └── profile ├── profile.go ├── profile_test.go └── service.go /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | .DS_Store 3 | 4 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 5 | *.o 6 | *.a 7 | *.so 8 | 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 The GAEGo Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SAEGoL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # auth 2 | 3 | Package auth provides multi-provider Authentication. 4 | 5 | ### install ### 6 | `` 7 | go get github.com/gaego/auth 8 | `` 9 | -------------------------------------------------------------------------------- /appengine_openid/appengine_openid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package auth/appengine_openid provides authentication through 7 | Google App Engine OpenID 8 | */ 9 | package appengine_openid 10 | 11 | import ( 12 | aeuser "appengine/user" 13 | "github.com/gaego/auth/profile" 14 | "github.com/gaego/context" 15 | "github.com/gaego/person" 16 | "net/http" 17 | ) 18 | 19 | type Provider struct { 20 | Name, URL string 21 | } 22 | 23 | // New creates a New provider. 24 | func New() *Provider { 25 | return &Provider{ 26 | "AppEngineOpenID", 27 | "appengine.google.com", 28 | } 29 | } 30 | 31 | // Authenticate process the request and returns a populated UserProfile. 32 | // If the Authenticate method can not authenticate the User based on the 33 | // request, an error or a redirect URL wll be return. 34 | func (p *Provider) Authenticate(w http.ResponseWriter, r *http.Request) ( 35 | up *profile.Profile, redirectURL string, err error) { 36 | 37 | c := context.NewContext(r) 38 | 39 | url := r.FormValue("provider") 40 | // Set provider info. 41 | up = profile.New(p.Name, url) 42 | 43 | // Check for current User. 44 | 45 | u := aeuser.Current(c) 46 | 47 | if u == nil { 48 | redirectURL := r.URL.Path + "/callback" 49 | loginUrl, err := aeuser.LoginURLFederated(c, redirectURL, url) 50 | return up, loginUrl, err 51 | } 52 | 53 | if u.FederatedIdentity != "" { 54 | up.ID = u.FederatedIdentity 55 | } else { 56 | up.ID = u.ID 57 | } 58 | 59 | per := new(person.Person) 60 | per.Email = u.Email 61 | per.Emails = []*person.PersonEmails{ 62 | &person.PersonEmails{true, "home", u.Email}, 63 | } 64 | per.URL = u.FederatedIdentity 65 | up.Person = per 66 | 67 | return up, "", nil 68 | } 69 | -------------------------------------------------------------------------------- /appengine_openid/appengine_openid_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package appengine_openid 6 | 7 | import ( 8 | "github.com/gaego/auth" 9 | "github.com/gaego/context" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | ) 14 | 15 | func setUp() {} 16 | 17 | func tearDown() { 18 | context.Close() 19 | } 20 | 21 | func TestAuthenticate(t *testing.T) { 22 | setUp() 23 | defer tearDown() 24 | 25 | w := httptest.NewRecorder() 26 | 27 | // Register. 28 | 29 | pro := New() 30 | auth.Register("appengine_openid", pro) 31 | 32 | // Round 1: Now User. 33 | 34 | req, _ := http.NewRequest("GET", 35 | "http://localhost:8080/-/auth/appengine_openid?provider=gmail.com", nil) 36 | 37 | // Process. 38 | 39 | _, url, err := pro.Authenticate(w, req) 40 | 41 | if url == "" { 42 | exampleURL := 43 | "/_ah/login?continue=http%3A//127.0.0.1%3A51002/-/auth/appengine_openid/callback" 44 | t.Errorf(`url: %v, want: %v`, url, exampleURL) 45 | } 46 | if err != nil { 47 | t.Errorf(`err: %v, want: %v`, err, nil) 48 | } 49 | // TODO: appenginetesting does not allow headers to passed to the 50 | // request. This will have to go non tested for the time being. 51 | 52 | // // Round 2: Mock User. 53 | // 54 | // req, _ = http.NewRequest("GET", 55 | // "http://localhost:8080/-/auth/appengine_openid/callback", nil) 56 | // req.Header.Set("Content-Type", "application/x-www-form-urlencoded;") 57 | // 58 | // req.Header.Set("X-AppEngine-Inbound-User-Email", "test@example.org") 59 | // req.Header.Set("X-AppEngine-Inbound-User-Federated-Identity", "gmail.com") 60 | // req.Header.Set("X-AppEngine-Inbound-User-Federated-Provider", "google") 61 | // req.Header.Set("X-AppEngine-Inbound-User-Id", "12345") 62 | // req.Header.Set("X-AppEngine-Inbound-User-Is-Admin", "0") 63 | // 64 | // // Process. 65 | // 66 | // up = user_profile.New() 67 | // url, err = pro.Authenticate(w, req, up) 68 | // 69 | // // Check. 70 | // 71 | // t.Fatalf(`up.Person: %v`, up.Person) 72 | // 73 | // if x := up.ProviderURL; x != "gmail.com" { 74 | // t.Errorf(`ProviderURL: %q, want %v`, x, "gmail.com") 75 | // } 76 | // if x := up.Person.Emails[0].Value; x != "test@example.org" { 77 | // t.Errorf(`Email.Value: %v, want %v`, x, "test@example.org") 78 | // } 79 | } 80 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package auth provides multi-provider Authentication. 7 | 8 | Example Usage: 9 | 10 | import ( 11 | "github.com/gaego/auth" 12 | "github.com/gaego/auth/google" 13 | ) 14 | 15 | // Register the Google Provider. 16 | googleProvider := google.Provider.New("12345", "ABCD") 17 | Register("google", &googleProvider) 18 | // Register additional providers. 19 | // ... 20 | 21 | 22 | */ 23 | package auth 24 | 25 | import ( 26 | "github.com/gaego/auth/profile" 27 | "github.com/gaego/context" 28 | "github.com/gaego/user" 29 | "net/http" 30 | "strings" 31 | ) 32 | 33 | var ( 34 | // BaseURL represents the base url to be used for providers. For 35 | // example if the base url is /auth/ all provider urls would be at 36 | // /auth/ 37 | BaseURL = "/-/auth/" 38 | // LoginURL is a string representing the URL to be redirected to on 39 | // errors. 40 | LoginURL = "/-/auth/login" 41 | // LogoutURL is a string representing the URL to be used to remove 42 | // the auth cookie. 43 | LogoutURL = "/-/auth/logout" 44 | // SuccessURL is a string representing the URL to be direct to on a 45 | // successful login. 46 | SuccessURL = "/" 47 | ) 48 | 49 | var providers = make(map[string]authenticater) 50 | 51 | type authenticater interface { 52 | Authenticate(http.ResponseWriter, *http.Request) (*profile.Profile, string, error) 53 | } 54 | 55 | // Register adds an Authenticater for the auth service. 56 | // 57 | // It takes a string which is used for the url, and a pointer to an 58 | // authentication provider that implements Authenticater. 59 | // E.g. 60 | // 61 | // googleProvider := google.Provider.New("12345", "ABCD") 62 | // Register("google", &googleProvider) 63 | // 64 | func Register(key string, auth authenticater) { 65 | providers[key] = auth 66 | // Set the start url e.g. /-/auth/google to be handled by the handler. 67 | http.HandleFunc(BaseURL+key, handler) 68 | // Set the callback url e.g. /-/auth/google/callback to be handled by the handler. 69 | http.HandleFunc(BaseURL+key+"/callback", handler) 70 | } 71 | 72 | // breakURL parse an url and returns the provider key. If the URL is 73 | // invalid it returns and empty string "". 74 | func breakURL(url string) (name string) { 75 | if p := strings.Split(url, BaseURL); len(p) > 1 { 76 | name = strings.Split(p[1], "/")[0] 77 | } 78 | return 79 | } 80 | 81 | // CreateAndLogin does the following: 82 | // 83 | // - Search for an existing user - session -> Profile -> email address 84 | // - Saves the Profile to the datastore 85 | // - Creates a User or appends the AuthID to the Requesting user's account 86 | // - Logs in the User 87 | // - Adds the admin role to the User if they are an GAE Admin. 88 | func CreateAndLogin(w http.ResponseWriter, r *http.Request, 89 | p *profile.Profile) (u *user.User, err error) { 90 | c := context.NewContext(r) 91 | if u, err = p.UpdateUser(w, r); err != nil { 92 | return 93 | } 94 | if err = user.CurrentUserSetID(w, r, p.UserID); err != nil { 95 | return 96 | } 97 | err = p.Put(c) 98 | return 99 | } 100 | 101 | func handler(w http.ResponseWriter, r *http.Request) { 102 | var url string 103 | var err error 104 | var up *profile.Profile 105 | k := breakURL(r.URL.Path) 106 | p := providers[k] 107 | if up, url, err = p.Authenticate(w, r); err != nil { 108 | // TODO: set error message in session. 109 | http.Redirect(w, r, LoginURL, http.StatusFound) 110 | return 111 | } 112 | // If we have a url the Provider wants to make a redirect before 113 | // proceeding. 114 | if url != "" { 115 | http.Redirect(w, r, url, http.StatusFound) 116 | return 117 | } 118 | // If we don't have a URL or an error then the user has been authenticated. 119 | // Check the Profile for an ID and Provider. 120 | if up.ID == "" || up.ProviderName == "" { 121 | panic(`auth: The Profile's "ID" or "ProviderName" is empty.` + 122 | `A Key can not be created.`) 123 | } 124 | if _, err = CreateAndLogin(w, r, up); err != nil { 125 | // TODO: set error message in session. 126 | http.Redirect(w, r, LoginURL, http.StatusFound) 127 | return 128 | } 129 | // If we've made it this far redirect to the SuccessURL 130 | http.Redirect(w, r, SuccessURL, http.StatusFound) 131 | return 132 | } 133 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package auth 6 | 7 | import ( 8 | "appengine/datastore" 9 | "errors" 10 | "github.com/gaego/auth/dev" 11 | "github.com/gaego/auth/profile" 12 | "github.com/gaego/context" 13 | "github.com/gaego/user" 14 | "net/http" 15 | "net/http/httptest" 16 | "testing" 17 | ) 18 | 19 | func setup() { 20 | BaseURL = "/-/auth/" 21 | LoginURL = "/-/auth/login" 22 | LogoutURL = "/-/auth/logout" 23 | SuccessURL = "/" 24 | } 25 | 26 | func teardown() { 27 | context.Close() 28 | } 29 | 30 | type TestProvider struct { 31 | dev.Provider 32 | } 33 | 34 | func (p *TestProvider) Authenticate(w http.ResponseWriter, r *http.Request) ( 35 | up *profile.Profile, url string, err error) { 36 | return nil, "/redirect-to-url", nil 37 | } 38 | 39 | type TPRedirect struct { 40 | dev.Provider 41 | } 42 | 43 | func (p *TPRedirect) Authenticate(w http.ResponseWriter, r *http.Request) ( 44 | up *profile.Profile, url string, err error) { 45 | return nil, "/redirect-to-url", nil 46 | } 47 | 48 | type TPError struct { 49 | dev.Provider 50 | } 51 | 52 | func (p *TPError) Authenticate(w http.ResponseWriter, r *http.Request) ( 53 | up *profile.Profile, url string, err error) { 54 | err = errors.New("Mock error") 55 | return nil, "", err 56 | } 57 | 58 | type TPComplete struct { 59 | dev.Provider 60 | } 61 | 62 | func (p *TPComplete) Authenticate(w http.ResponseWriter, r *http.Request) ( 63 | up *profile.Profile, url string, err error) { 64 | up = profile.New("Example", "example.com") 65 | up.ID = "1" 66 | return up, "", nil 67 | } 68 | 69 | func TestNew(t *testing.T) { 70 | setup() 71 | defer teardown() 72 | 73 | x := &TestProvider{} 74 | x.Name = "Example" 75 | x.URL = "http://exaple.com" 76 | // Confirm that it implements authenticater 77 | var y interface{} = x 78 | p, ok := y.(authenticater) 79 | if !ok { 80 | t.Errorf(`p = %q,"`, p) 81 | } 82 | } 83 | 84 | func TestBreakURL(t *testing.T) { 85 | setup() 86 | defer teardown() 87 | 88 | url1 := "http://localhost:8080/-/auth/example1" 89 | n := breakURL(url1) 90 | if n != "example1" { 91 | t.Errorf(`n: %q, want example1`, n) 92 | } 93 | url2 := "http://localhost:8080/-/auth/example2/callback?some=crazy[stuff]" 94 | n2 := breakURL(url2) 95 | if n2 != "example2" { 96 | t.Errorf(`n: %q, want example2`, n2) 97 | } 98 | // Change the BaseURL 99 | BaseURL = "/changed/" 100 | url3 := "http://localhost:8080/changed/example3/callback?some=crazy[stuff]" 101 | n3 := breakURL(url3) 102 | if n3 != "example3" { 103 | t.Errorf(`n: %q, want example3`, n3) 104 | } 105 | } 106 | 107 | func TestRedirect(t *testing.T) { 108 | setup() 109 | defer teardown() 110 | 111 | // Register 112 | 113 | p := &TPRedirect{} 114 | Register("example2", p) 115 | r, _ := http.NewRequest("GET", "http://localhost:8080/-/auth/example2", nil) 116 | w := httptest.NewRecorder() 117 | 118 | // Run it through the auth handler. 119 | 120 | handler(w, r) 121 | 122 | // Inspected the redirect. 123 | 124 | hdr := w.Header() 125 | if hdr["Location"][0] != "/redirect-to-url" { 126 | t.Errorf(`hdr["Location"]: %q, want "/redirect-to-url"`, hdr["Location"]) 127 | } 128 | } 129 | 130 | func TestError(t *testing.T) { 131 | setup() 132 | defer teardown() 133 | 134 | // Register 135 | 136 | p := &TPError{} 137 | Register("example3", p) 138 | r, _ := http.NewRequest("GET", "http://localhost:8080/-/auth/example3", nil) 139 | w := httptest.NewRecorder() 140 | 141 | // Run it through the auth handler. 142 | 143 | handler(w, r) 144 | 145 | // Inspected the redirect. 146 | 147 | hdr := w.Header() 148 | if hdr["Location"][0] != LoginURL { 149 | t.Errorf(`hdr["Location"]: %q, want %q`, hdr["Location"], LoginURL) 150 | } 151 | } 152 | 153 | func Test_handler(t *testing.T) { 154 | setup() 155 | defer teardown() 156 | _ = context.NewContext(nil) 157 | 158 | // Register the Provider 159 | 160 | p := &TPComplete{} 161 | Register("example5", p) 162 | r, _ := http.NewRequest("GET", "http://localhost:8080/-/auth/example5", nil) 163 | w := httptest.NewRecorder() 164 | 165 | // Run it through the auth handler. 166 | 167 | handler(w, r) 168 | 169 | // Inspected the redirect. 170 | 171 | hdr := w.Header() 172 | if hdr["Location"][0] != SuccessURL { 173 | t.Errorf(`hdr["Location"]: %q, want %q`, hdr["Location"][0], SuccessURL) 174 | t.Errorf(`w: %q`, w) 175 | t.Errorf(`hdr: %q`, hdr) 176 | } 177 | } 178 | 179 | func Test_CreateAndLogin(t *testing.T) { 180 | setup() 181 | defer teardown() 182 | c := context.NewContext(nil) 183 | 184 | up := profile.New("Example", "example.com") 185 | r, _ := http.NewRequest("GET", "http://localhost:8080/-/auth/example4", nil) 186 | w := httptest.NewRecorder() 187 | 188 | // Round 1: No User | No Profile 189 | 190 | // Confirm. 191 | 192 | q := datastore.NewQuery("User") 193 | if cnt, _ := q.Count(c); cnt != 0 { 194 | t.Errorf(`User cnt: %v, want 0`, cnt) 195 | } 196 | q = datastore.NewQuery("Profile") 197 | if cnt, _ := q.Count(c); cnt != 0 { 198 | t.Errorf(`Profile cnt: %v, want 0`, cnt) 199 | } 200 | u, err := user.Current(r) 201 | if err != user.ErrNoLoggedInUser { 202 | t.Errorf(`err: %v, want %v`, err, user.ErrNoLoggedInUser) 203 | } 204 | 205 | // Create. 206 | 207 | up.ID = "1" 208 | up.ProviderName = "Example" 209 | up.SetKey(c) 210 | u, err = CreateAndLogin(w, r, up) 211 | if err != nil { 212 | t.Errorf(`err: %v, want nil`, err) 213 | } 214 | 215 | if u.Key.StringID() != "1" { 216 | t.Errorf(`u.Key.StringID(): %v, want 1`, u.Key.StringID()) 217 | } 218 | if up.Key.StringID() != "example|1" { 219 | t.Errorf(`up.Key.StringID(): %v, want "example|1"`, up.Key.StringID()) 220 | } 221 | if up.UserID != u.Key.StringID() { 222 | t.Errorf(`up.UserID: %v, want %v`, up.UserID, u.Key.StringID()) 223 | } 224 | 225 | // Confirm Profile. 226 | 227 | rup, err := profile.Get(c, "example|1") 228 | if err != nil { 229 | t.Errorf(`err: %v, want nil`, err) 230 | } 231 | if rup.ID != "1" { 232 | t.Errorf(`rup.ID: %v, want "1"`, rup.ID) 233 | } 234 | if rup.Key.StringID() != "example|1" { 235 | t.Errorf(`rup.Key.StringID(): %v, want "example|1"`, rup.Key.StringID()) 236 | } 237 | if rup.UserID != u.Key.StringID() { 238 | t.Errorf(`rup.UserID: %v, want %v`, rup.UserID, u.Key.StringID()) 239 | } 240 | 241 | // Confirm User. 242 | 243 | ru, err := user.Get(c, "1") 244 | if err != nil { 245 | t.Fatalf(`err: %v, want nil`, err) 246 | } 247 | if ru.AuthIDs[0] != "example|1" { 248 | t.Errorf(`ru.AuthIDs[0]: %v, want "example|1"`, ru.AuthIDs[0]) 249 | } 250 | if ru.Key.StringID() != "1" { 251 | t.Errorf(`ru.Key.StringID(): %v, want 1`, ru.Key.StringID()) 252 | } 253 | q2 := datastore.NewQuery("User") 254 | q4 := datastore.NewQuery("AuthProfile") 255 | 256 | // Confirm Logged in User. 257 | 258 | u, err = user.Current(r) 259 | if err != nil { 260 | t.Errorf(`err: %v, want %v`, err, nil) 261 | } 262 | if u.Key.StringID() != "1" { 263 | t.Errorf(`u.Key.StringID(): %v, want 1`, u.Key.StringID()) 264 | } 265 | if len(u.AuthIDs) != 1 { 266 | t.Errorf(`len(u.AuthIDs): %v, want 1`, len(u.AuthIDs)) 267 | t.Errorf(`u.AuthIDs: %v`, u.AuthIDs) 268 | t.Errorf(`u: %v`, u) 269 | } 270 | 271 | // Round 2: Logged in User | Second Profile 272 | 273 | // Create. 274 | 275 | up = profile.New("AnotherExample", "anotherexample.com") 276 | up.ID = "2" 277 | up.SetKey(c) 278 | u, err = CreateAndLogin(w, r, up) 279 | if err != nil { 280 | t.Errorf(`err: %v, want nil`, err) 281 | } 282 | 283 | // Confirm Profile. 284 | 285 | rup, err = profile.Get(c, "anotherexample|2") 286 | if err != nil { 287 | t.Errorf(`err: %v, want nil`, err) 288 | } 289 | if rup.ID != "2" { 290 | t.Errorf(`rup.ID: %v, want "2"`, rup.ID) 291 | } 292 | if rup.Key.StringID() != "anotherexample|2" { 293 | t.Errorf(`rup.Key.StringID(): %v, want "anotherexample|2"`, rup.Key.StringID()) 294 | } 295 | if rup.UserID != u.Key.StringID() { 296 | t.Errorf(`rup.UserID: %v, want %v`, rup.UserID, u.Key.StringID()) 297 | } 298 | 299 | // Confirm Logged in User hasn't changed. 300 | 301 | u, err = user.Current(r) 302 | if err != nil { 303 | t.Errorf(`err: %v, want %v`, err, nil) 304 | } 305 | if u.Key.StringID() != "1" { 306 | t.Errorf(`u.Key.StringID(): %v, want 1`, u.Key.StringID()) 307 | } 308 | if len(u.AuthIDs) != 2 { 309 | t.Errorf(`len(u.AuthIDs): %v, want 2`, len(u.AuthIDs)) 310 | t.Errorf(`u.AuthIDs: %v`, u.AuthIDs) 311 | t.Errorf(`u: %v`, u) 312 | } 313 | if u.AuthIDs[0] != "example|1" { 314 | t.Errorf(`u.AuthIDs[0]: %v, want "example|1"`, u.AuthIDs[0]) 315 | } 316 | if u.AuthIDs[1] != "anotherexample|2" { 317 | t.Errorf(`u.AuthIDs[1]: %v, want "anotherexample|2"`, u.AuthIDs[1]) 318 | } 319 | 320 | // Confirm Counts 321 | 322 | q2 = datastore.NewQuery("User") 323 | if cnt, _ := q2.Count(c); cnt != 1 { 324 | t.Errorf(`User cnt: %v, want 1`, cnt) 325 | } 326 | q4 = datastore.NewQuery("AuthProfile") 327 | 328 | // Round 3: Logged out User | Existing Profile 329 | 330 | err = user.Logout(w, r) 331 | if err != nil { 332 | t.Errorf(`err: %v, want nil`, err) 333 | } 334 | 335 | // Confirm Logged out User. 336 | 337 | u, err = user.Current(r) 338 | if err != user.ErrNoLoggedInUser { 339 | t.Errorf(`err: %q, want %q`, err, user.ErrNoLoggedInUser) 340 | } 341 | 342 | // Login. 343 | 344 | up = profile.New("Example", "example.com") 345 | up.ID = "1" 346 | up.SetKey(c) 347 | u, err = CreateAndLogin(w, r, up) 348 | if err != nil { 349 | t.Errorf(`err: %v, want nil`, err) 350 | } 351 | 352 | // Confirm. 353 | 354 | q2 = datastore.NewQuery("User") 355 | if cnt, _ := q2.Count(c); cnt != 1 { 356 | t.Errorf(`User cnt: %v, want 1`, cnt) 357 | } 358 | q4 = datastore.NewQuery("AuthProfile") 359 | if cnt, _ := q4.Count(c); cnt != 2 { 360 | t.Errorf(`Profile cnt: %v, want 1`, cnt) 361 | } 362 | 363 | // Confirm Logged in User hasn't changed. 364 | 365 | u, err = user.Current(r) 366 | if err != nil { 367 | t.Errorf(`err: %v, want %v`, err, nil) 368 | } 369 | if u.Key.StringID() != "1" { 370 | t.Errorf(`u.Key.StringID(): %v, want "1"`, u.Key.StringID()) 371 | } 372 | if len(u.AuthIDs) != 2 { 373 | t.Errorf(`len(u.AuthIDs): %v, want 2`, len(u.AuthIDs)) 374 | t.Errorf(`u.AuthIDs: %s`, u.AuthIDs) 375 | t.Errorf(`u: %v`, u) 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /dev/dev.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package auth/dev provides a developement strategy for testing. 7 | */ 8 | package dev 9 | 10 | import ( 11 | "github.com/gorilla/schema" 12 | "github.com/gaego/auth/profile" 13 | "github.com/gaego/person" 14 | "net/http" 15 | ) 16 | 17 | type Provider struct { 18 | Name, URL string 19 | } 20 | 21 | // New creates a New provider. 22 | func New() *Provider { 23 | return &Provider{"Dev", "http://localhost:8080"} 24 | } 25 | 26 | // Authenticate process the request and returns a populated Profile. 27 | // If the Authenticate method can not authenticate the User based on the 28 | // request, an error or a redirect URL wll be return. 29 | func (p *Provider) Authenticate(w http.ResponseWriter, r *http.Request) ( 30 | up *profile.Profile, url string, err error) { 31 | 32 | up = profile.New(p.Name, p.URL) 33 | // Add the User's Unique ID. If an ID is not provided make this 34 | // value "default" 35 | up.ID = r.FormValue("ID") 36 | if up.ID == "" { 37 | up.ID = "default" 38 | } 39 | 40 | // Decode the form data and add the resulting Person type to the Profile. 41 | per := &person.Person{} 42 | decoder := schema.NewDecoder() 43 | decoder.Decode(per, r.Form) 44 | up.Person = per 45 | 46 | return up, "", nil 47 | } 48 | -------------------------------------------------------------------------------- /dev/dev_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package dev 6 | 7 | import ( 8 | "github.com/gaego/auth" 9 | "github.com/gaego/context" 10 | "net/http" 11 | "net/http/httptest" 12 | "net/url" 13 | "strings" 14 | "testing" 15 | ) 16 | 17 | func setUp() {} 18 | 19 | func tearDown() { 20 | context.Close() 21 | } 22 | 23 | func TestAuthenticate(t *testing.T) { 24 | setUp() 25 | defer tearDown() 26 | 27 | w := httptest.NewRecorder() 28 | 29 | // Register. 30 | 31 | pro := New() 32 | auth.Register("dev", pro) 33 | 34 | // Post. 35 | v := url.Values{} 36 | v.Set("ID", "1") 37 | v.Set("Gender", "male") 38 | v.Set("Name.GivenName", "Barack") 39 | v.Set("Name.FamilyName", "Obama") 40 | v.Set("AboutMe", "This is a bio about me.") 41 | body := strings.NewReader(v.Encode()) 42 | 43 | req, _ := http.NewRequest("POST", "http://localhost:8080/-/auth/dev", body) 44 | 45 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded;") 46 | 47 | // TODO(kylefinley) for some reason is if this isn't call here the form will 48 | // be empty in the Authentication method? Perhaps this is a bug. 49 | if id := req.FormValue("ID"); id != "1" { 50 | t.Errorf(`req.FormValue("ID") = %q, want "1"`, id) 51 | } 52 | 53 | // Process. 54 | 55 | up, url, err := pro.Authenticate(w, req) 56 | 57 | // Check. 58 | 59 | if url != "" { 60 | t.Errorf(`url: %v, want: ""`, url) 61 | } 62 | if err != nil { 63 | t.Errorf(`err: %v, want: %v`, err, nil) 64 | } 65 | 66 | per := up.Person 67 | 68 | if x := per.Name.GivenName; x != "Barack" { 69 | t.Errorf(`per.Name.GivenName: %q, want %v`, x, "Barack") 70 | } 71 | if x := per.Name.FamilyName; x != "Obama" { 72 | t.Errorf(`per.Name.FamilyName: %q, want %v`, x, "Obama") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /facebook/facebook.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package auth/facebook provides Facebook authentication 7 | */ 8 | package facebook 9 | 10 | import ( 11 | "github.com/gaego/auth/oauth2" 12 | ) 13 | 14 | const ( 15 | CLIENT_ID = "343417275669983" 16 | CLIENT_SECRET = "fec59504f33b238a5d7b5f3b35bd958a" 17 | PROFILE_URL = "https://graph.facebook.com/me" 18 | ) 19 | 20 | type Provider struct { 21 | oauth2.Provider 22 | } 23 | 24 | func New(clientID, clientSecret, scope string) *Provider { 25 | return &Provider{ 26 | Provider: oauth2.Provider{ 27 | Name: "Facebook", 28 | URL: "http://facebook.com", 29 | ClientID: clientID, 30 | ClientSecret: clientSecret, 31 | Scope: scope, 32 | AuthURL: "https://graph.facebook.com/oauth/authorize", 33 | TokenURL: "https://graph.facebook.com/oauth/access_token", 34 | }, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /github/github.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package auth/github provides Github authentication 7 | */ 8 | package github 9 | 10 | import ( 11 | "github.com/gaego/auth/oauth2" 12 | ) 13 | 14 | const ( 15 | CLIENT_ID = "dbac99a147b10e6bc813" 16 | CLIENT_SECRET = "5f6e11429eeef14d0fe79721ee53459963e306f5" 17 | ) 18 | 19 | type Provider struct { 20 | oauth2.Provider 21 | } 22 | 23 | func New(clientID, clientSecret, scope string) *Provider { 24 | return &Provider{ 25 | Provider: oauth2.Provider{ 26 | Name: "Github", 27 | URL: "http://github.com", 28 | ClientID: clientID, 29 | ClientSecret: clientSecret, 30 | Scope: "", 31 | AuthURL: "https://github.com/login/oauth/authorize", 32 | TokenURL: "https://github.com/login/oauth/access_token", 33 | }, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /google/google.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Copyright: 2011 Google Inc. All Rights Reserved. 6 | // license: Apache Software License, see LICENSE for details. 7 | 8 | /* 9 | Package auth/google provides Google authentication 10 | */ 11 | package google 12 | 13 | import ( 14 | "github.com/gaego/auth/oauth2" 15 | ) 16 | 17 | type Provider struct { 18 | oauth2.Provider 19 | } 20 | 21 | func New(clientID, clientSecret, scope string) *Provider { 22 | return &Provider{ 23 | Provider: oauth2.Provider{ 24 | Name: "Google", 25 | URL: "https://plus.google.com", 26 | ClientID: clientID, 27 | ClientSecret: clientSecret, 28 | Scope: scope, 29 | AuthURL: "https://accounts.google.com/o/oauth2/auth", 30 | TokenURL: "https://accounts.google.com/o/oauth2/token", 31 | }, 32 | } 33 | } 34 | 35 | // func init() { 36 | // defaultCnfg = map[string]string{ 37 | // "BaseURL": baseURL, 38 | // "LoginURL": loginURL, 39 | // "LogoutURL": loginURL, 40 | // "SuccessURL": successURL, 41 | // } 42 | // } 43 | // 44 | // // setConfig retrieves the global config for the "auth" key and sets 45 | // // local variables based on the response. 46 | // func setConfig(c appengine.Context) { 47 | // // TODO(kylefinley): This doesn't work. The cnft needs to be set before 48 | // // a context/request is available. 49 | // cnfg, err := config.GetOrInsert(c, "auth", defaultCnfg) 50 | // if err != nil { 51 | // panic("auth: an error occured while setting the config") 52 | // } 53 | // baseURL = cnfg.Values["BaseURL"] 54 | // loginURL = cnfg.Values["LoginURL"] 55 | // logoutURL = cnfg.Values["LogoutURL"] 56 | // successURL = cnfg.Values["SuccessURL"] 57 | // } 58 | 59 | // func (p *UserProfile) PersonRaw(c appengine.Context) interface{} { 60 | // 61 | // // There's a bug where Google Plus doesn"t return an email address. 62 | // // So we'll retrieve it the old way and inject it into res. 63 | // // We're also checking to se if this account is a legacy account, 64 | // // in which case we"ll perform the legacy user lookup. 65 | // is_legacy = False 66 | // res = {} 67 | // try: 68 | // res = self.service().people().get(userId="me").execute(self.http()) 69 | // except Exception, e: 70 | // is_legacy = True 71 | // if is_legacy or "emails" not in res: 72 | // service = self.service(name="oauth2", version="v1") 73 | // legacy_res = service.userinfo().get().execute(self.http()) 74 | // 75 | // email = { 76 | // "value": legacy_res.get("email"), 77 | // "primary": True, 78 | // "verified": legacy_res.get("verified_email")} 79 | // res["emails"] = [email] 80 | // 81 | // if "displayName" not in res: 82 | // res["displayName"] = legacy_res.get("name") 83 | // 84 | // if "name" not in res: 85 | // res["name"] = { 86 | // "givenName": legacy_res.get("given_name"), 87 | // "familyName": legacy_res.get("family_name"), 88 | // } 89 | // 90 | // if "url" not in res: 91 | // res["url"] = legacy_res.get("link") 92 | // 93 | // if "image" not in res: 94 | // res["image"] = {"url": legacy_res.get("picture")} 95 | // 96 | // if "locale" not in res: 97 | // res["locale"] = legacy_res.get("locale") 98 | // return res 99 | // 100 | // } 101 | -------------------------------------------------------------------------------- /google/google_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package google 6 | -------------------------------------------------------------------------------- /oauth2/oauth2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package auth/oauth2 provides OAuth2 authentication 7 | */ 8 | package oauth2 9 | 10 | import ( 11 | "appengine/urlfetch" 12 | "code.google.com/p/goauth2/oauth" 13 | "fmt" 14 | "github.com/gaego/auth/profile" 15 | "github.com/gaego/context" 16 | "net/http" 17 | "net/url" 18 | "strings" 19 | ) 20 | 21 | type Provider struct { 22 | Name string 23 | URL string 24 | ClientID string 25 | ClientSecret string 26 | Scope string 27 | AuthURL string 28 | TokenURL string 29 | RedirectURL string 30 | } 31 | 32 | func New(name, url, clientID, clientSecret, scope, authURL, tokenURL string) *Provider { 33 | return &Provider{ 34 | Name: name, 35 | URL: url, 36 | ClientID: clientID, 37 | ClientSecret: clientSecret, 38 | Scope: scope, 39 | AuthURL: authURL, 40 | TokenURL: tokenURL, 41 | } 42 | } 43 | 44 | // Config returns the configuration information for OAuth2. 45 | func (p *Provider) Config(url *url.URL) *oauth.Config { 46 | return &oauth.Config{ 47 | ClientId: p.ClientID, 48 | ClientSecret: p.ClientSecret, 49 | Scope: p.Scope, 50 | AuthURL: p.AuthURL, 51 | TokenURL: p.TokenURL, 52 | RedirectURL: fmt.Sprintf("%s://%s/-/auth/%s/callback", url.Scheme, url.Host, 53 | strings.ToLower(p.Name)), 54 | } 55 | } 56 | 57 | func (p *Provider) start(r *http.Request) string { 58 | return p.Config(r.URL).AuthCodeURL(r.URL.RawQuery) 59 | } 60 | 61 | func (p *Provider) callback(r *http.Request) error { 62 | // Exchange code for an access token at OAuth provider. 63 | code := r.FormValue("code") 64 | t := &oauth.Transport{ 65 | Config: p.Config(r.URL), 66 | Transport: &urlfetch.Transport{ 67 | Context: context.NewContext(r), 68 | }, 69 | } 70 | _, err := t.Exchange(code) 71 | return err 72 | } 73 | 74 | func (p *Provider) Authenticate(r *http.Request) ( 75 | up *profile.Profile, redirectURL string, err error) { 76 | 77 | //c := context.NewContext(r) 78 | return up, "", nil 79 | } 80 | -------------------------------------------------------------------------------- /oauth2/oauth2_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package oauth2 6 | 7 | import ( 8 | //"github.com/gaego/auth/provider" 9 | "net/url" 10 | "testing" 11 | ) 12 | 13 | func setup() { 14 | 15 | } 16 | func tearDown() { 17 | 18 | } 19 | 20 | type ExampleProvider struct { 21 | OAuth2Provider 22 | Provider 23 | Config 24 | ClientID string 25 | ClientSecret string 26 | Scope string 27 | AuthURL string 28 | TokenURL string 29 | RedirectURL string 30 | } 31 | 32 | func TestProvider(clientID, clientSecret, scope string) *ExampleProvider { 33 | return &ExampleProvider{ 34 | ClientID: clientID, 35 | ClientSecret: clientSecret, 36 | Scope: scope, 37 | AuthURL: "http://example.com/auth", 38 | TokenURL: "http://example.com/token", 39 | } 40 | } 41 | 42 | func NewExamplePrvider(clientID, clientSecret, scope string) *ExampleProvider { 43 | return &ExampleProvider{ 44 | OAuth2Provider.ClientID: clientID, 45 | OAuth2Provider.ClientSecret: clientSecret, 46 | OAuth2Provider.Scope: scope, 47 | OAuth2Provider.AuthURL: "http://example.com/auth", 48 | OAuth2Provider.TokenURL: "http://example.com/token", 49 | } 50 | } 51 | 52 | func (p *ExampleProvider) GetProviderName() interface{} { 53 | return "Good Bye" 54 | } 55 | 56 | func TestConfig(t *testing.T) { 57 | //e := new(ExampleProvider) 58 | u := &url.URL{ 59 | Host: "test.com", 60 | Scheme: "http", 61 | } 62 | p := NewProvider("Test", "http://test.com", "123", "abc", "email", 63 | "http://example.com/auth", "http://example.com/token") 64 | p.Config(u) 65 | 66 | if x := p.RedirectURL; x == "http://test.com/-/auth/test/callback" { 67 | t.Errorf(`RedirctURL: %v, want %v`, x, "http://test.com/-/auth/test/callback") 68 | } 69 | e := ExampleProvider{ 70 | OAuth2Provider: OAuth2Provider{ 71 | Name: "Google", 72 | ClientID: "12345", 73 | ClientSecret: "password", 74 | Scope: "email", 75 | }, 76 | } 77 | e := ExampleProvider{ 78 | Provider: Provider{ 79 | Name: "Google", 80 | }, 81 | Config: Config{ 82 | ClientID: "12345", 83 | ClientSecret: "password", 84 | Scope: "email", 85 | }, 86 | } 87 | e := &ExampleProvider{Provider: Provider{Name: "Google"}} 88 | e.OAuth2Provider.ClientSecret = "1234" 89 | c := e.Config.Get("test.com") 90 | t.Errorf(` %q"`, e.Name) 91 | c := e.SayName() 92 | t.Errorf(` %q"`, c) 93 | ep := NewExamplePrvider("12345", "password", "email") 94 | if ep.AuthURL != "http://example.com/auth" { 95 | t.Errorf(`ep.AuthURL = %q, want "http://example.com/auth"`, 96 | x.AuthURL) 97 | } 98 | } 99 | 100 | func TestAuthenticate(t *testing.T) { 101 | setUp() 102 | defer tearDown() 103 | 104 | w := httptest.NewRecorder() 105 | 106 | // Register. 107 | 108 | pro := New() 109 | auth.Register("dev", pro) 110 | 111 | // Post. 112 | v := url.Values{} 113 | v.Set("ID", "1") 114 | v.Set("Gender", "male") 115 | v.Set("Name.GivenName", "Barack") 116 | v.Set("Name.FamilyName", "Obama") 117 | v.Set("AboutMe", "This is a bio about me.") 118 | body := strings.NewReader(v.Encode()) 119 | 120 | req, _ := http.NewRequest("POST", "http://localhost:8080/", body) 121 | 122 | //req.Header.Set("Content-Type", "application/x-www-form-urlencoded;") 123 | 124 | // TODO(kylefinley) for some reason is if this isn't call here the form will 125 | // be empty in the Authentication method? Perhaps this is a bug. 126 | if id := req.FormValue("ID"); id != "1" { 127 | t.Errorf(`req.FormValue("ID") = %q, want "1"`, id) 128 | } 129 | 130 | // Process. 131 | 132 | up := profile.New() 133 | url, err := pro.Authenticate(w, req, up) 134 | 135 | // Check. 136 | 137 | if url != "" { 138 | t.Errorf(`url: %v, want: ""`, url) 139 | } 140 | if err != nil { 141 | t.Errorf(`err: %v, want: %v`, err, nil) 142 | } 143 | 144 | per, err := up.Person() 145 | 146 | if err != nil { 147 | t.Errorf(`err: %v, want: %v`, err, nil) 148 | } 149 | if x := per.Name.GivenName; x != "Barack" { 150 | t.Errorf(`per.Name.GivenName: %q, want %v`, x, "Barack") 151 | } 152 | if x := per.Name.FamilyName; x != "Obama" { 153 | t.Errorf(`per.Name.FamilyName: %q, want %v`, x, "Obama") 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /password/password.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package password 6 | 7 | import ( 8 | "appengine" 9 | "errors" 10 | "github.com/gaego/auth/profile" 11 | "github.com/gaego/person" 12 | "github.com/gaego/user" 13 | "github.com/gaego/user/email" 14 | "golang.org/x/crypto/bcrypt" 15 | ) 16 | 17 | var ( 18 | PasswordLengthMin = 4 19 | PasswordLengthMax = 31 20 | BryptCost = 12 21 | ) 22 | 23 | var ( 24 | ErrPasswordMismatch = errors.New("auth/password: passwords do not match") 25 | ErrPasswordLength = errors.New("auth/password: passwords must be between 4 and 31 charaters") 26 | ) 27 | 28 | type Password struct { 29 | New string `json:"new,omitempty"` 30 | Current string `json:"current,omitempty"` 31 | IsSet bool `json:"isSet"` 32 | Email string `json:"email"` 33 | } 34 | 35 | // validatePasswordLength returns true if the supplied string is 36 | // between 4 and 31 character. 37 | func Validate(p string) error { 38 | if len(p) < PasswordLengthMin { 39 | return ErrPasswordLength 40 | } 41 | if len(p) > PasswordLengthMax { 42 | return ErrPasswordLength 43 | } 44 | return nil 45 | } 46 | 47 | func (p *Password) Validate() (err error) { 48 | // Validate pasword 49 | if p.New != "" { 50 | if err = Validate(p.New); err != nil { 51 | return 52 | } 53 | } 54 | if p.Current != "" { 55 | if err = Validate(p.Current); err != nil { 56 | return 57 | } 58 | } 59 | // Validate email 60 | if err = email.Validate(p.Email); err != nil { 61 | return 62 | } 63 | return 64 | } 65 | 66 | func GenerateFromPassword(password []byte) ([]byte, error) { 67 | return bcrypt.GenerateFromPassword(password, BryptCost) 68 | } 69 | 70 | func CompareHashAndPassword(hash, password []byte) error { 71 | if bcrypt.CompareHashAndPassword(hash, password) != nil { 72 | return ErrPasswordMismatch 73 | } 74 | return nil 75 | } 76 | 77 | func authenticate(c appengine.Context, pass *Password, pers *person.Person, userID string) ( 78 | pf *profile.Profile, err error) { 79 | 80 | if err = pass.Validate(); err != nil { 81 | return nil, err 82 | } 83 | if pass.New != "" && pass.Current != "" { 84 | pf, err = update(c, pass.Current, pass.New, userID, pers) 85 | return 86 | } 87 | if pass.New != "" { 88 | // if we have a user ID check for a profile 89 | if userID != "" { 90 | if pf, err = login(c, pass.New, userID); err == ErrProfileNotFound { 91 | pf, err = create(c, pass.New, pers, userID) 92 | return 93 | } 94 | if err != nil { 95 | return 96 | } 97 | } 98 | pf, err = create(c, pass.New, pers, "") 99 | return 100 | } 101 | if pass.Current != "" { 102 | pf, err = login(c, pass.Current, userID) 103 | return 104 | } 105 | return pf, nil 106 | } 107 | 108 | func create(c appengine.Context, pass string, pers *person.Person, userID string) ( 109 | pf *profile.Profile, err error) { 110 | 111 | var id string 112 | if userID == "" { 113 | u := user.New() 114 | u.SetKey(c) 115 | if err = u.Put(c); err != nil { 116 | return 117 | } 118 | id = u.Key.StringID() 119 | } else { 120 | id = userID 121 | } 122 | pf = profile.New("Password", "") 123 | pf.ID = id 124 | pf.UserID = id 125 | pf.Auth, _ = GenerateFromPassword([]byte(pass)) 126 | pf.Person = pers 127 | return 128 | } 129 | 130 | func login(c appengine.Context, pass string, userID string) ( 131 | pf *profile.Profile, err error) { 132 | 133 | if userID == "" { 134 | return nil, ErrProfileNotFound 135 | } 136 | pid := profile.GenAuthID("Password", userID) 137 | if pf, err = profile.Get(c, pid); err != nil { 138 | return nil, ErrProfileNotFound 139 | } 140 | if err := CompareHashAndPassword(pf.Auth, []byte(pass)); err != nil { 141 | return nil, err 142 | } 143 | return pf, nil 144 | } 145 | 146 | func update(c appengine.Context, passCurrent, passNew string, userID string, pers *person.Person) ( 147 | pf *profile.Profile, err error) { 148 | 149 | if pf, err = login(c, passCurrent, userID); err != nil { 150 | return 151 | } 152 | pf.Auth, _ = GenerateFromPassword([]byte(passNew)) 153 | pf.Person = pers 154 | return pf, nil 155 | } 156 | -------------------------------------------------------------------------------- /password/password_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package password 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestValidate(t *testing.T) { 12 | if x := Validate("pas"); x != ErrPasswordLength { 13 | t.Errorf(`validatePass("pas") = %v, want false`, x) 14 | } 15 | if x := Validate("passw"); x != nil { 16 | t.Errorf(`validatePass("passw") = %v, want true`, x) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /password/provider.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package auth/password provides a password strategy using bcrypt. 7 | 8 | auth/password stategy takes a POST with the following keys: 9 | 10 | Email (required) 11 | Password.New (required/optional) 12 | Password.Current (required/optional) 13 | Name.GivenName 14 | Name.FamilyName 15 | * (Any other Person attributes) 16 | 17 | Based on the supplied attributes auth/password will do one of three things: 18 | 19 | 1. To Create new User and log them in. POST: 20 | - "Email" 21 | - "Password.New" (present) 22 | - "Password.Current" (NOT present) 23 | - + Person attributes, E.g. "Name.GivenName", "Name.FamilyName" 24 | 25 | 2. To Login User or return error if password does not match. POST: 26 | - "Email" 27 | - "Password.Current" (present) 28 | - "Password.New" (NOT present) 29 | 30 | 3. To Update Password / Person details. POST: 31 | - "Email" 32 | - "Password.New" (present) 33 | - "Password.Current" (present) 34 | - + Person attributes, E.g. "Name.GivenName", "Name.FamilyName" 35 | 36 | */ 37 | package password 38 | 39 | import ( 40 | "github.com/gorilla/schema" 41 | "errors" 42 | "github.com/gaego/auth/profile" 43 | "github.com/gaego/context" 44 | "github.com/gaego/person" 45 | "github.com/gaego/user" 46 | "net/http" 47 | ) 48 | 49 | var ( 50 | ErrProfileNotFound = errors.New("auth/password: profile not found for email address") 51 | ) 52 | 53 | // Provider represents the auth.Provider 54 | type Provider struct { 55 | Name, URL string 56 | } 57 | 58 | // New creates a New provider. 59 | func New() *Provider { 60 | return &Provider{"Password", ""} 61 | } 62 | 63 | func decodePerson(r *http.Request) *person.Person { 64 | // Decode the form data and add the resulting Person type to the Profile. 65 | p := &person.Person{} 66 | decoder := schema.NewDecoder() 67 | decoder.Decode(p, r.Form) 68 | return p 69 | } 70 | 71 | // Authenticate process the request and returns a populated Profile. 72 | // If the Authenticate method can not authenticate the User based on the 73 | // request, an error or a redirect URL wll be return. 74 | func (p *Provider) Authenticate(w http.ResponseWriter, r *http.Request) ( 75 | pf *profile.Profile, url string, err error) { 76 | 77 | p.URL = r.URL.Host 78 | pf = profile.New(p.Name, p.URL) 79 | 80 | pass := &Password{ 81 | New: r.FormValue("Password.New"), 82 | Current: r.FormValue("Password.Current"), 83 | Email: r.FormValue("Email"), 84 | } 85 | c := context.NewContext(r) 86 | userID, _ := user.CurrentUserIDByEmail(r, pass.Email) 87 | pers := decodePerson(r) 88 | pf, err = authenticate(c, pass, pers, userID) 89 | return pf, "", err 90 | } 91 | -------------------------------------------------------------------------------- /password/provider_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package password 6 | 7 | import ( 8 | "github.com/gaego/auth" 9 | "github.com/gaego/auth/profile" 10 | "github.com/gaego/context" 11 | "github.com/gaego/user/email" 12 | "github.com/gaego/person" 13 | "github.com/gaego/user" 14 | "net/http" 15 | "net/http/httptest" 16 | "net/url" 17 | "strings" 18 | "sync" 19 | "testing" 20 | ) 21 | 22 | var startOnce sync.Once 23 | 24 | func setup() *Provider { 25 | // Register. 26 | pro := New() 27 | startOnce.Do(func() { 28 | auth.Register("password", pro) 29 | }) 30 | return pro 31 | } 32 | 33 | func tearDown() { 34 | context.Close() 35 | } 36 | 37 | func createRequest(v url.Values) *http.Request { 38 | body := strings.NewReader(v.Encode()) 39 | req, _ := http.NewRequest("POST", 40 | "http://localhost:8080/-/auth/password", body) 41 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded;") 42 | return req 43 | } 44 | 45 | func TestAuthenticate_Validate(t *testing.T) { 46 | pro := setup() 47 | defer tearDown() 48 | 49 | var err error 50 | var v url.Values 51 | var r *http.Request 52 | 53 | w := httptest.NewRecorder() 54 | 55 | // Check invalid POST 56 | v = url.Values{} 57 | v.Set("Email", "fake") 58 | v.Set("Password.New", "bad") 59 | r = createRequest(v) 60 | // Check. 61 | if _, _, err = pro.Authenticate(w, r); err == nil { 62 | t.Errorf(`Invalid Email or Password should result in an error`) 63 | } 64 | } 65 | 66 | // Scenario #1: 67 | // - No User session 68 | // - No Email Saved 69 | // - No Profile Saved 70 | func TestAuthenticate_Scenario1(t *testing.T) { 71 | pro := setup() 72 | defer tearDown() 73 | 74 | var pf *profile.Profile 75 | var uRL string 76 | var err error 77 | var v url.Values 78 | var r *http.Request 79 | 80 | w := httptest.NewRecorder() 81 | 82 | // Post. 83 | v = url.Values{} 84 | v.Set("Email", "test@example.org") 85 | v.Set("Password.New", "secret1") 86 | v.Set("Name.GivenName", "Barack") 87 | r = createRequest(v) 88 | // Check. 89 | if pf, uRL, err = pro.Authenticate(w, r); uRL != "" || err != nil { 90 | t.Errorf(`url: %v, want: ""`, uRL) 91 | t.Errorf(`err: %v, want: %v`, err, nil) 92 | } 93 | if x := pf.Person.Name.GivenName; x != "Barack" { 94 | t.Errorf(`pf.Person.Name.GivenName: %v, want %v`, x, "Barack") 95 | } 96 | if x := pf.UserID; x == "" { 97 | t.Errorf(`pf.UserID: %v, want %v`, x, "Some ID") 98 | } 99 | if x := pf.ID; x == "" { 100 | t.Errorf(`pf.ID: %v, want %v`, x, "Some ID") 101 | } 102 | } 103 | 104 | // Scenario #2: 105 | // - No User session 106 | // - Yes Email Saved 107 | // - Yes Profile Saved 108 | func TestAuthenticate_Scenario2(t *testing.T) { 109 | pro := setup() 110 | defer tearDown() 111 | 112 | var pf *profile.Profile 113 | var uRL string 114 | var err error 115 | var v url.Values 116 | var r *http.Request 117 | 118 | c := context.NewContext(nil) 119 | w := httptest.NewRecorder() 120 | 121 | // Profile Not found 122 | v = url.Values{} 123 | v.Set("Email", "test@example.org") 124 | v.Set("Password.Current", "secret1") 125 | r = createRequest(v) 126 | // Check. 127 | if pf, uRL, err = pro.Authenticate(w, r); uRL != "" || err != ErrProfileNotFound { 128 | t.Errorf(`url: %v, want: ""`, uRL) 129 | t.Errorf(`err: %v, want: %v`, err, ErrProfileNotFound) 130 | } 131 | 132 | // Setup. 133 | pf = profile.New("Password", "") 134 | pf.UserID = "1" 135 | pf.ID = "1" 136 | passHash, _ := GenerateFromPassword([]byte("secret1")) 137 | pf.Auth = passHash 138 | pf.SetKey(c) 139 | pf.Person = &person.Person{ 140 | Name: &person.PersonName{ 141 | GivenName: "Barack", 142 | FamilyName: "Obama", 143 | }, 144 | } 145 | _ = pf.Put(c) 146 | e := email.New() 147 | e.UserID = "1" 148 | e.SetKey(c, "test@example.org") 149 | _ = e.Put(c) 150 | 151 | // 1. Login 152 | // a. Correct password. 153 | v = url.Values{} 154 | v.Set("Email", "test@example.org") 155 | v.Set("Password.Current", "secret1") 156 | v.Set("Name.GivenName", "Berry") 157 | r = createRequest(v) 158 | // Check. 159 | if pf, uRL, err = pro.Authenticate(w, r); uRL != "" || err != nil { 160 | t.Errorf(`url: %v, want: ""`, uRL) 161 | t.Fatalf(`err: %v, want: %v`, err, nil) 162 | } 163 | if x := pf.Person.Name.GivenName; x != "Barack" { 164 | t.Errorf(`.Person should not be updated on login`) 165 | } 166 | // b. In-Correct password. 167 | v = url.Values{} 168 | v.Set("Email", "test@example.org") 169 | v.Set("Password.Current", "fakepass") 170 | r = createRequest(v) 171 | // Check. 172 | if _, _, err = pro.Authenticate(w, r); err != ErrPasswordMismatch { 173 | t.Errorf(`err: %v, want: %v`, err, ErrPasswordMismatch) 174 | } 175 | // 2. Update 176 | // a. Correct password. 177 | v = url.Values{} 178 | v.Set("Email", "test@example.org") 179 | v.Set("Password.Current", "secret1") 180 | v.Set("Password.New", "secret2") 181 | v.Set("Name.GivenName", "Berry") 182 | r = createRequest(v) 183 | // Check. 184 | if pf, uRL, err = pro.Authenticate(w, r); uRL != "" || err != nil { 185 | t.Errorf(`url: %v, want: ""`, uRL) 186 | t.Errorf(`err: %v, want: %v`, err, nil) 187 | } 188 | if x := pf.Person.Name.GivenName; x != "Berry" { 189 | t.Errorf(`pf.Person should be updated on update`) 190 | } 191 | if x := pf.UserID; x != "1" { 192 | t.Errorf(`pf.UserID: %v, want %v`, x, "1") 193 | } 194 | if err := CompareHashAndPassword(pf.Auth, []byte("secret2")); err != nil { 195 | t.Errorf(`Password was not changed`) 196 | } 197 | // b. In-Correct password. 198 | v = url.Values{} 199 | v.Set("Email", "test@example.org") 200 | v.Set("Password.Current", "fakepass") 201 | v.Set("Password.New", "hacked") 202 | v.Set("Name.GivenName", "Bob") 203 | r = createRequest(v) 204 | // Check. 205 | if _, _, err = pro.Authenticate(w, r); err != ErrPasswordMismatch { 206 | t.Errorf(`err: %v, want: %v`, err, ErrPasswordMismatch) 207 | } 208 | // 2. Create - Should login user 209 | // a. Correct password. 210 | v = url.Values{} 211 | v.Set("Email", "test@example.org") 212 | v.Set("Password.New", "secret1") 213 | v.Set("Name.GivenName", "Bob1") 214 | r = createRequest(v) 215 | // Check. 216 | if pf, uRL, err = pro.Authenticate(w, r); uRL != "" || err != nil { 217 | t.Errorf(`url: %v, want: ""`, uRL) 218 | t.Errorf(`err: %v, want: %v`, err, nil) 219 | } 220 | if x := pf.Person.Name.GivenName; x != "Bob1" { 221 | t.Errorf(`.Person should be updated on update`) 222 | } 223 | if x := pf.UserID; x != "1" { 224 | t.Errorf(`pf.UserID: %v, want %v`, x, "1") 225 | } 226 | if err := CompareHashAndPassword(pf.Auth, []byte("secret1")); err != nil { 227 | t.Errorf(`Password was not changed`) 228 | } 229 | // b. In-Correct password. 230 | v = url.Values{} 231 | v.Set("Email", "test@example.org") 232 | v.Set("Password.New", "fakepass") 233 | v.Set("Name.GivenName", "Bob2") 234 | r = createRequest(v) 235 | // Check. 236 | if _, _, err = pro.Authenticate(w, r); err != ErrPasswordMismatch { 237 | t.Errorf(`err: %v, want: %v`, err, ErrPasswordMismatch) 238 | } 239 | } 240 | 241 | // Scenario #3: 242 | // - Yes User session 243 | // - No Email Saved 244 | // - No Profile Saved 245 | func TestAuthenticate_Scenario3(t *testing.T) { 246 | pro := setup() 247 | defer tearDown() 248 | 249 | var pf *profile.Profile 250 | var uRL string 251 | var err error 252 | var v url.Values 253 | var r *http.Request 254 | 255 | //c := context.NewContext(nil) 256 | w := httptest.NewRecorder() 257 | 258 | // Setup. 259 | v = url.Values{} 260 | v.Set("Email", "test@example.org") 261 | v.Set("Password.New", "secret1") 262 | v.Set("Name.GivenName", "Bob") 263 | r = createRequest(v) 264 | _ = user.CurrentUserSetID(w, r, "1001") 265 | 266 | // Check. 267 | if pf, uRL, err = pro.Authenticate(w, r); uRL != "" || err != nil { 268 | t.Errorf(`url: %v, want: ""`, uRL) 269 | t.Errorf(`err: %v, want: %v`, err, nil) 270 | } 271 | if x := pf.Person.Name.GivenName; x != "Bob" { 272 | t.Errorf(`pf.Person should be updated`) 273 | } 274 | if x := pf.UserID; x != "1001" { 275 | t.Errorf(`pf.UserID: %v, want %v`, x, "1001") 276 | } 277 | if pf.UserID != "1001" { 278 | t.Errorf(`pf.UserID: %v, want: %v`, pf.UserID, "1001") 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /password/service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package password 6 | 7 | import ( 8 | "github.com/gaego/auth" 9 | "github.com/gaego/auth/profile" 10 | "github.com/gaego/context" 11 | "github.com/gaego/person" 12 | "github.com/gaego/user" 13 | "net/http" 14 | ) 15 | 16 | type Service struct{} 17 | 18 | type Args struct { 19 | Password *Password 20 | Person *person.Person 21 | } 22 | 23 | func (s *Service) Authenticate(w http.ResponseWriter, r *http.Request, 24 | args *Args, reply *Args) (err error) { 25 | 26 | c := context.NewContext(r) 27 | args.Person.Email = args.Password.Email 28 | userID, _ := user.CurrentUserIDByEmail(r, args.Password.Email) 29 | pf, err := authenticate(c, args.Password, args.Person, userID) 30 | if err != nil { 31 | return err 32 | } 33 | if _, err = auth.CreateAndLogin(w, r, pf); err != nil { 34 | return err 35 | } 36 | reply.Person = pf.Person 37 | return nil 38 | } 39 | 40 | // Current returns the current users password object minus the password 41 | func (s *Service) Current(w http.ResponseWriter, r *http.Request, 42 | args *Args, reply *Args) (err error) { 43 | 44 | c := context.NewContext(r) 45 | var isSet bool 46 | userID, _ := user.CurrentUserID(r) 47 | _, err = profile.Get(c, profile.GenAuthID("Password", userID)) 48 | if err == nil { 49 | isSet = true 50 | } 51 | reply.Password = &Password{IsSet: isSet} 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /password/service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package password 6 | -------------------------------------------------------------------------------- /profile/profile.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The AEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Copyright: 2011 Google Inc. All Rights Reserved. 6 | // license: Apache Software License, see LICENSE for details. 7 | 8 | /* 9 | Package auth/profile provides the auth.Profile for starage of 10 | authentication strategies. 11 | */ 12 | package profile 13 | 14 | import ( 15 | "appengine" 16 | "appengine/datastore" 17 | aeuser "appengine/user" 18 | "encoding/json" 19 | "errors" 20 | "fmt" 21 | "github.com/gaego/context" 22 | "github.com/gaego/ds" 23 | "github.com/gaego/person" 24 | "github.com/gaego/user" 25 | "net/http" 26 | "strings" 27 | "time" 28 | ) 29 | 30 | type Profile struct { 31 | // Key is the datastore key. It is not saved back to the datastore 32 | // it is just embeded here for convience. 33 | Key *datastore.Key `datastore:"-"` 34 | // ID represents a unique ID from the Provider. 35 | // This ID does not have to be unique to this application just to the 36 | // provider. 37 | ID string 38 | // A String representing the Provider that performed the 39 | // authentication. The Provider should be in the proper case for 40 | // example a User who was authenticated through Google should have 41 | // "Google" here and not "google" 42 | ProviderName string 43 | // ProviderURL is the URL that is commonly accepted as the 44 | // originator of the authentication. For example Google plus would 45 | // be http://plus.google.com and not http://google.com. 46 | ProviderURL string `datastore:",noindex"` 47 | // UserID is the string ID of the User that the Profile belongs to. 48 | UserID string 49 | // Auth maybe used by the provodier to store any information that it 50 | // may need. 51 | Auth []byte 52 | // Person is an Object representing personal information about the user. 53 | Person *person.Person `datastore:"-"` 54 | // PersonJSON is the Person object converted to JSON, for storage purposes. 55 | PersonJSON []byte `datastore:"Person"` 56 | // PersonRawJSON is the JSON encoded representation of the raw 57 | // response returned from a provider representing the User's Profile. 58 | PersonRawJSON []byte 59 | // Created is a time.Time representing with the Profile was created. 60 | Created time.Time 61 | // Created is a time.Time representing with the Profile was updated. 62 | Updated time.Time 63 | } 64 | 65 | // New creates a new Profile and set the Created to now 66 | func New(providerName, providerURL string) *Profile { 67 | return &Profile{ 68 | ProviderName: providerName, 69 | ProviderURL: providerURL, 70 | Person: new(person.Person), 71 | Created: time.Now(), 72 | Updated: time.Now(), 73 | } 74 | } 75 | 76 | // GenAuthID generates a unique id for the Profile based on a string 77 | // representing the provider and a unique id provided by the provider. 78 | // Using this generator is prefered over compiling the key manually to 79 | // ensure consistency. 80 | func GenAuthID(provider, id string) string { 81 | return fmt.Sprintf("%s|%s", strings.ToLower(provider), id) 82 | } 83 | 84 | // newKey generates a *datastore.Key based on a string representing 85 | // the provider and a unique id provided by the provider. 86 | func newKey(c appengine.Context, provider, id string) *datastore.Key { 87 | authID := GenAuthID(provider, id) 88 | return datastore.NewKey(c, "AuthProfile", authID, 0, nil) 89 | } 90 | 91 | // SetKey creates and embeds a ds.Key to the entity. 92 | func (u *Profile) SetKey(c appengine.Context) (err error) { 93 | u.Key = newKey(c, u.ProviderName, u.ID) 94 | return 95 | } 96 | 97 | // Encode is called prior to save. Any fields that need to be updated 98 | // prior to save are updated here. 99 | func (u *Profile) Encode() error { 100 | // Update Person 101 | 102 | // Sanity check, TODO maybe we should raise an error instead. 103 | if u.Person == nil { 104 | u.Person = new(person.Person) 105 | } 106 | u.Person.Provider = &person.PersonProvider{ 107 | Name: u.ProviderName, 108 | URL: u.ProviderURL, 109 | } 110 | u.Person.Kind = fmt.Sprintf("%s#person", strings.ToLower(u.ProviderName)) 111 | u.Person.ID = u.ID 112 | // TODO(kylefinley) consider alternatives to returning miliseconds. 113 | // Convert time to unix miliseconds for javascript 114 | u.Person.Created = u.Created.UnixNano() / 1000000 115 | u.Person.Updated = u.Updated.UnixNano() / 1000000 116 | // Convert to JSON 117 | j, err := json.Marshal(u.Person) 118 | u.PersonJSON = j 119 | return err 120 | } 121 | 122 | // Decode is called after the entity has been retrieved from the the ds. 123 | func (u *Profile) Decode() error { 124 | if u.PersonJSON != nil { 125 | var p *person.Person 126 | err := json.Unmarshal(u.PersonJSON, &p) 127 | u.Person = p 128 | return err 129 | } 130 | return nil 131 | } 132 | 133 | // Get is a convience method for retrieveing an entity from the ds. 134 | func Get(c appengine.Context, id string) (up *Profile, err error) { 135 | up = &Profile{} 136 | key := datastore.NewKey(c, "AuthProfile", id, 0, nil) 137 | err = ds.Get(c, key, up) 138 | up.Key = key 139 | up.Decode() 140 | return 141 | } 142 | 143 | func GetMulti(c appengine.Context, ids []string) (pfs []*Profile, err error) { 144 | key := make([]*datastore.Key, len(ids)) 145 | for k, id := range ids { 146 | key[k] = datastore.NewKey(c, "AuthProfile", id, 0, nil) 147 | } 148 | pfs = make([]*Profile, len(ids)) 149 | for i := range pfs { 150 | pfs[i] = new(Profile) 151 | } 152 | err = ds.GetMulti(c, key, pfs) 153 | for i := range pfs { 154 | pfs[i].Key = key[i] 155 | pfs[i].Decode() 156 | } 157 | return 158 | } 159 | 160 | func GetPersonMulti(c appengine.Context, ids []string) (pers []*person.Person, err error) { 161 | pfs, err := GetMulti(c, ids) 162 | pers = make([]*person.Person, len(pfs)) 163 | for i, pf := range pfs { 164 | pers[i] = pf.Person 165 | } 166 | return 167 | } 168 | 169 | // Put is a convience method to save the Profile to the datastore and 170 | // updated the Updated property to time.Now(). 171 | func (u *Profile) Put(c appengine.Context) error { 172 | // TODO add error handeling for empty Provider and ID 173 | u.SetKey(c) 174 | u.Updated = time.Now() 175 | u.Encode() 176 | key, err := ds.Put(c, u.Key, u) 177 | u.Key = key 178 | return err 179 | } 180 | 181 | // UpdateUser does the following: 182 | // - Search for an existing user - session -> Profile -> email address 183 | // - Creates a User or appends the AuthID to the Requesting user's account 184 | // - Adds the admin role to the User if they are a GAE Admin. 185 | func (p *Profile) UpdateUser(w http.ResponseWriter, r *http.Request) (u *user.User, err error) { 186 | 187 | c := context.NewContext(r) 188 | if p.Key == nil { 189 | if p.ProviderName == "" && p.ID == "" { 190 | return nil, errors.New("auth: key not set") 191 | } 192 | p.SetKey(c) 193 | } 194 | var saveUser bool // flag indicating that the user needs to be saved. 195 | 196 | // Find the UserID 197 | // if the AuthProfile doesn't have a UserID look it up. And populate the 198 | // UserID from the saved profile. 199 | if p.UserID == "" { 200 | if p2, err := Get(c, p.Key.StringID()); err == nil { 201 | p.UserID = p2.UserID 202 | } 203 | } 204 | // look up the UserID in the session 205 | currentUserID, _ := user.CurrentUserID(r) 206 | if currentUserID != "" { 207 | if p.UserID == "" { 208 | p.UserID = currentUserID 209 | } else { 210 | // TODO: User merge 211 | } 212 | } 213 | 214 | // If we still don't have a UserID create a new user 215 | if p.UserID == "" { 216 | // Create User 217 | u = user.New() 218 | // Allocation an new ID 219 | if err = u.SetKey(c); err != nil { 220 | return nil, err 221 | } 222 | saveUser = true 223 | } else { 224 | if u, err = user.Get(c, p.UserID); err != nil { 225 | // if user is not found we have some type of syncing problem. 226 | c.Criticalf(`auth: userID: %v was saved to Profile / Session, but was not found in the datastore`, p.UserID) 227 | return nil, err 228 | } 229 | } 230 | // Add AuthID 231 | if err = u.AddAuthID(p.Key.StringID()); err == nil { 232 | saveUser = true 233 | } 234 | if p.Person.Email != "" { 235 | if _, err := u.AddEmail(c, p.Person.Email, 0); err == nil { 236 | saveUser = true 237 | } 238 | } 239 | // If current user is an admin in GAE add role to User 240 | if aeuser.IsAdmin(c) { 241 | // Save the roll to the session 242 | _ = user.CurrentUserSetRole(w, r, "admin", true) 243 | // Add the role to the user's roles. 244 | if err = u.AddRole("admin"); err == nil { 245 | saveUser = true 246 | } 247 | } 248 | if saveUser { 249 | if err = u.Put(c); err != nil { 250 | return nil, err 251 | } 252 | } 253 | p.UserID = u.Key.StringID() 254 | return u, nil 255 | } 256 | -------------------------------------------------------------------------------- /profile/profile_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The AEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package profile 6 | 7 | import ( 8 | "appengine/datastore" 9 | "github.com/gaego/context" 10 | "github.com/gaego/ds" 11 | "github.com/gaego/person" 12 | "testing" 13 | ) 14 | 15 | func tearDown() { 16 | context.Close() 17 | } 18 | 19 | func TestNewKey(t *testing.T) { 20 | c := context.NewContext(nil) 21 | defer tearDown() 22 | 23 | k1 := datastore.NewKey(c, "AuthProfile", "google|12345", 0, nil) 24 | k2 := newKey(c, "Google", "12345") 25 | if k1.String() != k2.String() { 26 | t.Errorf("k2: %q, want %q.", k2, k1) 27 | t.Errorf("k1:", k1) 28 | t.Errorf("k2:", k2) 29 | } 30 | } 31 | 32 | func TestGet(t *testing.T) { 33 | c := context.NewContext(nil) 34 | defer tearDown() 35 | 36 | // Save it. 37 | 38 | u := New("Google", "http://plus.google.com") 39 | u.ID = "12345" 40 | u.Person = &person.Person{ 41 | Name: &person.PersonName{ 42 | GivenName: "Barack", 43 | FamilyName: "Obama", 44 | }, 45 | } 46 | key := newKey(c, "google", "12345") 47 | u.Key = key 48 | err := u.Put(c) 49 | if err != nil { 50 | t.Errorf(`err: %q, want nil`, err) 51 | } 52 | 53 | // Get it. 54 | 55 | u2 := &Profile{} 56 | id := "google|12345" 57 | key = datastore.NewKey(c, "AuthProfile", id, 0, nil) 58 | err = ds.Get(c, key, u2) 59 | if err != nil { 60 | t.Errorf(`err: %q, want nil`, err) 61 | } 62 | u2, err = Get(c, id) 63 | if err != nil { 64 | t.Errorf(`err: %v, want nil`, err) 65 | } 66 | if u2.ID != "12345" { 67 | t.Errorf(`u2.ID: %v, want "1"`, u2.ID) 68 | } 69 | if u2.Key.StringID() != "google|12345" { 70 | t.Errorf(`uKey.StringID(): %v, want "google|12345"`, u2.Key.StringID()) 71 | } 72 | if x := u2.ProviderURL; x != "http://plus.google.com" { 73 | t.Errorf(`u2.ProviderURL: %v, want %s`, x, "http://plus.google.com") 74 | } 75 | if x := u2.Person.ID; x != "12345" { 76 | t.Errorf(`u2.Person.ID: %v, want %s`, x, "12345") 77 | } 78 | if x := u2.Person.Name.GivenName; x != "Barack" { 79 | t.Errorf(`u2.Person.Name.GivenName: %v, want %s`, x, "Barack") 80 | } 81 | if x := u2.Person.Provider.Name; x != "Google" { 82 | t.Errorf(`u2.Person.Provider.Name: %v, want %s`, x, "Google") 83 | } 84 | if x := u2.Person.Provider.URL; x != "http://plus.google.com" { 85 | t.Errorf(`u2.Person.Provider.URL: %v, want %s`, x, "http://plus.google.com") 86 | } 87 | if x := u2.Person.Kind; x != "google#person" { 88 | t.Errorf(`u2.Person.Kind: %v, want %s`, x, "google#person") 89 | } 90 | if u2.Person.Created != u2.Created.UnixNano()/1000000 { 91 | t.Errorf(`u2.Created: %v, want %v`, u2.Person.Created, 92 | u2.Created.UnixNano()/1000000) 93 | } 94 | if u2.Person.Updated != u2.Updated.UnixNano()/1000000 { 95 | t.Errorf(`u2.Updated: %v, want %v`, u2.Person.Updated, 96 | u2.Updated.UnixNano()/1000000) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /profile/service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 GAEGo Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package profile 6 | 7 | import ( 8 | "github.com/gaego/context" 9 | "github.com/gaego/person" 10 | "github.com/gaego/user" 11 | "net/http" 12 | ) 13 | 14 | type Service struct{} 15 | 16 | type Args struct{} 17 | 18 | type Reply struct { 19 | Profiles []*person.Person 20 | } 21 | 22 | func (s *Service) GetAll(w http.ResponseWriter, r *http.Request, 23 | args *Args, reply *Reply) (err error) { 24 | 25 | c := context.NewContext(r) 26 | u, err := user.Current(r) 27 | if err != nil { 28 | return err 29 | } 30 | if reply.Profiles, err = GetPersonMulti(c, u.AuthIDs); err != nil { 31 | return err 32 | } 33 | return nil 34 | } 35 | --------------------------------------------------------------------------------