auth

Paddy 2015-07-15 Parent:73e12d5a1124 Child:b7e685839a1b

178:0a2c3d677161 Go to Latest

auth/authcode_test.go

Update to use a generic event emitter. Rather can creating a purpose-built event emitter for each and every event we need to emit (I'm looking at you, login verification event) which is _downright silly_, we're now using a generic event publisher that's based on saying "HEY A MODEL UPDATED". This means we need to change all our setup code in authd to use events.NewNSQPublisher or events.NewStdoutPublisher instead of our homegrown solutions. Which also means updating our config to take an events.Publisher instead of our LoginVerificationNotifier (blergh). Our Context also now uses an events.Publisher instead of a LoginVerificationNotifier. Party all around! We also replaced our SendLoginVerification helper method on Context with a SendModelEvent helper method on Context, which is just a light wrapper around events.PublishModelEvent. Of course, all this means we need to update our email_verification listener to listen to the correct channel (based on the model we want updates about) and filter down to a Created action or our new custom action for "the customer wants their verification resent", which I'm OK making a special case and not generic, because c'mon. But we had a subtle change to all our constants, some of which are unofficial constants now. I'm unsure how I feel about this. We also updated our email_verification listener so that we're unmarshalling to a custom loginEvent, which is just an events.Event that overwrites the Data property to be an auth.Login instance. This is to make sure we don't need to wrangle a map[string]interface{}, which is no fun. I'm also OK with special-casing like this, because it's 1) a tiny amount of code, 2) properly utilising composition, and 3) the only way I can think of to cleanly accomplish what I want. I also added a note about GetLogin's deficient handling of logins, namely that it doesn't recognise admins and return Verification codes to them, which would be a useful property for internal tools to take advantage of. Ah well. I updated the Profile and Login implementations so they're now event.Model instances, mainly by just exporting some strings from them through getters that will let us automatically build an Event from them. This lets us use the PublishModelEvent helper. I updated our CreateProfileHandler to properly mangle the login Verification property, and to fire off the ActionCreated events for the new Login and the new Profile. I updated our GetLoginHandler and UpdateLoginHandler to properly mangle the loginVerification property. God that's annoying. :-/ You'll note I didn't start publishing the events.ActionUpdated or events.ActionDeleted events for Profiles or Logins yet, and didn't bother publishing any events for literally any other type. That's because I'm a lazy piece of crap and will end up publishing them when I absolutely have to. Part of that is because if a channel isn't created/being read for a topic, the messages will just stack up in NSQ, and I don't want that. But mostly I'm lazy. Finally, I got to delete the entire profile_verification.go file, because we're no longer special-casing that. Hooray!

History
paddy@29 1 package auth
paddy@29 2
paddy@29 3 import (
paddy@111 4 "bytes"
paddy@111 5 "io/ioutil"
paddy@111 6 "net/http"
paddy@111 7 "net/http/httptest"
paddy@111 8 "net/url"
paddy@156 9 "os"
paddy@111 10 "strings"
paddy@29 11 "testing"
paddy@29 12 "time"
paddy@29 13
paddy@107 14 "code.secondbit.org/uuid.hg"
paddy@29 15 )
paddy@29 16
paddy@156 17 func init() {
paddy@156 18 if os.Getenv("PG_TEST_DB") != "" {
paddy@156 19 p, err := NewPostgres(os.Getenv("PG_TEST_DB"))
paddy@156 20 if err != nil {
paddy@156 21 panic(err)
paddy@156 22 }
paddy@156 23 authCodeStores = append(authCodeStores, &p)
paddy@156 24 }
paddy@156 25 }
paddy@156 26
paddy@87 27 var authCodeStores = []authorizationCodeStore{NewMemstore()}
paddy@29 28
paddy@87 29 func compareAuthorizationCodes(authCode1, authCode2 AuthorizationCode) (success bool, field string, authCode1val, authCode2val interface{}) {
paddy@87 30 if authCode1.Code != authCode2.Code {
paddy@87 31 return false, "code", authCode1.Code, authCode2.Code
paddy@34 32 }
paddy@87 33 if !authCode1.Created.Equal(authCode2.Created) {
paddy@87 34 return false, "created", authCode1.Created, authCode2.Created
paddy@34 35 }
paddy@87 36 if authCode1.ExpiresIn != authCode2.ExpiresIn {
paddy@87 37 return false, "expires in", authCode1.ExpiresIn, authCode2.ExpiresIn
paddy@34 38 }
paddy@87 39 if !authCode1.ClientID.Equal(authCode2.ClientID) {
paddy@87 40 return false, "client ID", authCode1.ClientID, authCode2.ClientID
paddy@34 41 }
paddy@135 42 if len(authCode1.Scopes) != len(authCode2.Scopes) {
paddy@135 43 return false, "scopes", authCode1.Scopes, authCode2.Scopes
paddy@135 44 }
paddy@135 45 for pos, scope := range authCode1.Scopes {
paddy@135 46 if scope != authCode2.Scopes[pos] {
paddy@135 47 return false, "scopes", authCode1.Scopes, authCode2.Scopes
paddy@135 48 }
paddy@34 49 }
paddy@87 50 if authCode1.RedirectURI != authCode2.RedirectURI {
paddy@87 51 return false, "redirect URI", authCode1.RedirectURI, authCode2.RedirectURI
paddy@34 52 }
paddy@87 53 if authCode1.State != authCode2.State {
paddy@87 54 return false, "state", authCode1.State, authCode2.State
paddy@34 55 }
paddy@111 56 if !authCode1.ProfileID.Equal(authCode2.ProfileID) {
paddy@111 57 return false, "profile ID", authCode1.ProfileID, authCode2.ProfileID
paddy@111 58 }
paddy@111 59 if authCode1.Used != authCode2.Used {
paddy@111 60 return false, "used", authCode1.Used, authCode2.Used
paddy@111 61 }
paddy@34 62 return true, "", nil, nil
paddy@34 63 }
paddy@34 64
paddy@111 65 func TestAuthorizationCodeStore(t *testing.T) {
paddy@36 66 t.Parallel()
paddy@87 67 authCode := AuthorizationCode{
paddy@29 68 Code: "code",
paddy@149 69 Created: time.Now().Round(time.Millisecond),
paddy@29 70 ExpiresIn: 180,
paddy@29 71 ClientID: uuid.NewID(),
paddy@163 72 Scopes: stringsToScopes([]string{"scope"}),
paddy@29 73 RedirectURI: "redirectURI",
paddy@29 74 State: "state",
paddy@29 75 }
paddy@87 76 for _, store := range authCodeStores {
paddy@116 77 context := Context{authCodes: store}
paddy@116 78 err := context.SaveAuthorizationCode(authCode)
paddy@29 79 if err != nil {
paddy@87 80 t.Errorf("Error saving auth code to %T: %s", store, err)
paddy@34 81 }
paddy@116 82 err = context.SaveAuthorizationCode(authCode)
paddy@87 83 if err != ErrAuthorizationCodeAlreadyExists {
paddy@87 84 t.Errorf("Expected ErrAuthorizationCodeAlreadyExists from %T, got %+v", store, err)
paddy@29 85 }
paddy@116 86 retrieved, err := context.GetAuthorizationCode(authCode.Code)
paddy@29 87 if err != nil {
paddy@87 88 t.Errorf("Error retrieving auth code from %T: %s", store, err)
paddy@29 89 }
paddy@87 90 match, field, expectation, result := compareAuthorizationCodes(authCode, retrieved)
paddy@34 91 if !match {
paddy@87 92 t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
paddy@34 93 }
paddy@116 94 err = context.UseAuthorizationCode(authCode.Code)
paddy@111 95 if err != nil {
paddy@111 96 t.Errorf("Error retrieving auth code from %T: %s", store, err)
paddy@111 97 }
paddy@116 98 retrieved, err = context.GetAuthorizationCode(authCode.Code)
paddy@111 99 if err != nil {
paddy@111 100 t.Errorf("Error retrieving auth code from %T: %s", store, err)
paddy@111 101 }
paddy@111 102 authCode.Used = true
paddy@111 103 match, field, expectation, result = compareAuthorizationCodes(authCode, retrieved)
paddy@111 104 if !match {
paddy@111 105 t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
paddy@111 106 }
paddy@116 107 err = context.DeleteAuthorizationCode(authCode.Code)
paddy@29 108 if err != nil {
paddy@87 109 t.Errorf("Error removing auth code from %T: %s", store, err)
paddy@29 110 }
paddy@116 111 retrieved, err = context.GetAuthorizationCode(authCode.Code)
paddy@87 112 if err != ErrAuthorizationCodeNotFound {
paddy@87 113 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v and %+v", store, retrieved, err)
paddy@34 114 }
paddy@116 115 err = context.DeleteAuthorizationCode(authCode.Code)
paddy@87 116 if err != ErrAuthorizationCodeNotFound {
paddy@87 117 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
paddy@29 118 }
paddy@116 119 err = context.UseAuthorizationCode(authCode.Code)
paddy@111 120 if err != ErrAuthorizationCodeNotFound {
paddy@111 121 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
paddy@111 122 }
paddy@29 123 }
paddy@29 124 }
paddy@111 125
paddy@111 126 func TestAuthCodeGrantValidate(t *testing.T) {
paddy@111 127 t.Parallel()
paddy@111 128 store := NewMemstore()
paddy@111 129 testContext := Context{
paddy@111 130 clients: store,
paddy@111 131 authCodes: store,
paddy@111 132 profiles: store,
paddy@111 133 tokens: store,
paddy@111 134 sessions: store,
paddy@111 135 }
paddy@111 136 client := Client{
paddy@111 137 ID: uuid.NewID(),
paddy@111 138 Secret: "super secret!",
paddy@111 139 OwnerID: uuid.NewID(),
paddy@111 140 Name: "My test client",
paddy@111 141 Logo: "https://secondbit.org/logo.png",
paddy@111 142 Website: "https://secondbit.org/",
paddy@111 143 Type: "public",
paddy@111 144 }
paddy@111 145 endpoint := Endpoint{
paddy@111 146 ID: uuid.NewID(),
paddy@111 147 ClientID: client.ID,
paddy@116 148 URI: "https://test.secondbit.org/redirect",
paddy@149 149 Added: time.Now().Round(time.Millisecond),
paddy@111 150 }
paddy@116 151 err := testContext.SaveClient(client)
paddy@111 152 if err != nil {
paddy@111 153 t.Fatal("Can't store client:", err)
paddy@111 154 }
paddy@151 155 err = testContext.AddEndpoints([]Endpoint{endpoint})
paddy@111 156 if err != nil {
paddy@111 157 t.Fatal("Can't store endpoint:", err)
paddy@111 158 }
paddy@111 159 code := AuthorizationCode{
paddy@111 160 Code: "myauthcode",
paddy@149 161 Created: time.Now().Round(time.Millisecond),
paddy@111 162 ExpiresIn: 180,
paddy@111 163 ClientID: uuid.NewID(),
paddy@163 164 Scopes: stringsToScopes([]string{"scope"}),
paddy@111 165 RedirectURI: "redirectURI",
paddy@111 166 State: "state",
paddy@111 167 }
paddy@111 168 err = testContext.SaveAuthorizationCode(code)
paddy@111 169 if err != nil {
paddy@111 170 t.Fatal("Can't add auth code:", err)
paddy@111 171 }
paddy@112 172 code2 := code
paddy@112 173 code2.Code = "otherauthcode"
paddy@112 174 code2.ClientID = client.ID
paddy@112 175 err = testContext.SaveAuthorizationCode(code2)
paddy@112 176 if err != nil {
paddy@112 177 t.Fatal("Can't add second auth code:", err)
paddy@112 178 }
paddy@111 179 req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@111 180 if err != nil {
paddy@111 181 t.Fatal("Can't build request:", err)
paddy@111 182 }
paddy@112 183 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@111 184 w := httptest.NewRecorder()
paddy@111 185 params := url.Values{}
paddy@111 186 body := bytes.NewBufferString(params.Encode())
paddy@111 187 req.Body = ioutil.NopCloser(body)
paddy@111 188 scope, profileID, valid := authCodeGrantValidate(w, req, testContext)
paddy@111 189 if valid {
paddy@112 190 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
paddy@111 191 }
paddy@111 192 if w.Code != http.StatusBadRequest {
paddy@111 193 t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
paddy@111 194 }
paddy@112 195 expectedBody := `{"error":"invalid_request"}`
paddy@112 196 if strings.TrimSpace(w.Body.String()) != expectedBody {
paddy@112 197 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
paddy@112 198 }
paddy@112 199
paddy@112 200 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@112 201 if err != nil {
paddy@112 202 t.Fatal("Can't build request:", err)
paddy@112 203 }
paddy@112 204 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@112 205 w = httptest.NewRecorder()
paddy@112 206 params = url.Values{}
paddy@112 207 params.Set("code", "notmycode")
paddy@112 208 body = bytes.NewBufferString(params.Encode())
paddy@112 209 req.Body = ioutil.NopCloser(body)
paddy@112 210 err = req.ParseForm()
paddy@112 211 if err != nil {
paddy@112 212 t.Log(err)
paddy@112 213 }
paddy@112 214 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
paddy@112 215 if valid {
paddy@112 216 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
paddy@112 217 }
paddy@112 218 if w.Code != http.StatusUnauthorized {
paddy@112 219 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
paddy@112 220 }
paddy@112 221 expectedBody = `{"error":"invalid_client"}`
paddy@112 222 if expectedBody != strings.TrimSpace(w.Body.String()) {
paddy@112 223 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
paddy@112 224 }
paddy@112 225
paddy@112 226 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@112 227 if err != nil {
paddy@112 228 t.Fatal("Can't build request:", err)
paddy@112 229 }
paddy@112 230 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@112 231 req.SetBasicAuth(client.ID.String(), client.Secret)
paddy@112 232 w = httptest.NewRecorder()
paddy@112 233 params = url.Values{}
paddy@112 234 params.Set("code", "notmycode")
paddy@112 235 body = bytes.NewBufferString(params.Encode())
paddy@112 236 req.Body = ioutil.NopCloser(body)
paddy@112 237 err = req.ParseForm()
paddy@112 238 if err != nil {
paddy@112 239 t.Log(err)
paddy@112 240 }
paddy@112 241 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
paddy@112 242 if valid {
paddy@112 243 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
paddy@112 244 }
paddy@112 245 if w.Code != http.StatusBadRequest {
paddy@112 246 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
paddy@112 247 }
paddy@112 248 expectedBody = `{"error":"invalid_grant"}`
paddy@112 249 if expectedBody != strings.TrimSpace(w.Body.String()) {
paddy@112 250 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
paddy@112 251 }
paddy@112 252
paddy@112 253 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@112 254 if err != nil {
paddy@112 255 t.Fatal("Can't build request:", err)
paddy@112 256 }
paddy@112 257 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@112 258 req.SetBasicAuth(client.ID.String(), client.Secret)
paddy@112 259 w = httptest.NewRecorder()
paddy@112 260 params = url.Values{}
paddy@112 261 params.Set("code", code.Code)
paddy@112 262 params.Set("redirect_uri", "not my redirectURI")
paddy@112 263 body = bytes.NewBufferString(params.Encode())
paddy@112 264 req.Body = ioutil.NopCloser(body)
paddy@112 265 err = req.ParseForm()
paddy@112 266 if err != nil {
paddy@112 267 t.Log(err)
paddy@112 268 }
paddy@112 269 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
paddy@112 270 if valid {
paddy@112 271 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
paddy@112 272 }
paddy@112 273 if w.Code != http.StatusBadRequest {
paddy@112 274 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
paddy@112 275 }
paddy@112 276 expectedBody = `{"error":"invalid_grant"}`
paddy@112 277 if expectedBody != strings.TrimSpace(w.Body.String()) {
paddy@112 278 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
paddy@112 279 }
paddy@112 280
paddy@112 281 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@112 282 if err != nil {
paddy@112 283 t.Fatal("Can't build request:", err)
paddy@112 284 }
paddy@112 285 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@112 286 req.SetBasicAuth(client.ID.String(), client.Secret)
paddy@112 287 w = httptest.NewRecorder()
paddy@112 288 params = url.Values{}
paddy@112 289 params.Set("code", code.Code)
paddy@112 290 params.Set("redirect_uri", code.RedirectURI)
paddy@112 291 body = bytes.NewBufferString(params.Encode())
paddy@112 292 req.Body = ioutil.NopCloser(body)
paddy@112 293 err = req.ParseForm()
paddy@112 294 if err != nil {
paddy@112 295 t.Log(err)
paddy@112 296 }
paddy@112 297 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
paddy@112 298 if valid {
paddy@112 299 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
paddy@112 300 }
paddy@112 301 if w.Code != http.StatusBadRequest {
paddy@112 302 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
paddy@112 303 }
paddy@112 304 expectedBody = `{"error":"invalid_grant"}`
paddy@112 305 if expectedBody != strings.TrimSpace(w.Body.String()) {
paddy@112 306 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
paddy@112 307 }
paddy@112 308
paddy@112 309 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@112 310 if err != nil {
paddy@112 311 t.Fatal("Can't build request:", err)
paddy@112 312 }
paddy@112 313 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@112 314 req.SetBasicAuth(client.ID.String(), client.Secret)
paddy@112 315 w = httptest.NewRecorder()
paddy@112 316 params = url.Values{}
paddy@112 317 params.Set("code", code2.Code)
paddy@112 318 params.Set("redirect_uri", code2.RedirectURI)
paddy@112 319 body = bytes.NewBufferString(params.Encode())
paddy@112 320 req.Body = ioutil.NopCloser(body)
paddy@112 321 err = req.ParseForm()
paddy@112 322 if err != nil {
paddy@112 323 t.Log(err)
paddy@112 324 }
paddy@112 325 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
paddy@112 326 if !valid {
paddy@112 327 t.Fatalf("Expected valid auth code, was not valid.")
paddy@111 328 }
paddy@111 329 }
paddy@112 330
paddy@112 331 func TestAuthCodeGrantInvalidate(t *testing.T) {
paddy@112 332 t.Parallel()
paddy@112 333 store := NewMemstore()
paddy@112 334 testContext := Context{
paddy@112 335 clients: store,
paddy@112 336 authCodes: store,
paddy@112 337 profiles: store,
paddy@112 338 tokens: store,
paddy@112 339 sessions: store,
paddy@112 340 }
paddy@112 341 code := AuthorizationCode{
paddy@112 342 Code: "myauthcode",
paddy@149 343 Created: time.Now().Round(time.Millisecond),
paddy@112 344 ExpiresIn: 180,
paddy@112 345 ClientID: uuid.NewID(),
paddy@163 346 Scopes: stringsToScopes([]string{"scope"}),
paddy@112 347 RedirectURI: "redirectURI",
paddy@112 348 State: "state",
paddy@112 349 }
paddy@112 350 err := testContext.SaveAuthorizationCode(code)
paddy@112 351 if err != nil {
paddy@112 352 t.Fatal("Can't add auth code:", err)
paddy@112 353 }
paddy@112 354 req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@112 355 if err != nil {
paddy@112 356 t.Fatal("Can't build request:", err)
paddy@112 357 }
paddy@112 358 err = authCodeGrantInvalidate(req, testContext)
paddy@112 359 if err != ErrAuthorizationCodeNotFound {
paddy@112 360 t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
paddy@112 361 }
paddy@112 362 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@112 363 if err != nil {
paddy@112 364 t.Fatal("Can't build request:", err)
paddy@112 365 }
paddy@112 366 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@112 367 params := url.Values{}
paddy@112 368 params.Set("code", "notmycode")
paddy@112 369 body := bytes.NewBufferString(params.Encode())
paddy@112 370 req.Body = ioutil.NopCloser(body)
paddy@112 371 err = authCodeGrantInvalidate(req, testContext)
paddy@112 372 if err != ErrAuthorizationCodeNotFound {
paddy@112 373 t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
paddy@112 374 }
paddy@112 375 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@112 376 if err != nil {
paddy@112 377 t.Fatal("Can't build request:", err)
paddy@112 378 }
paddy@112 379 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@112 380 params.Set("code", code.Code)
paddy@112 381 body = bytes.NewBufferString(params.Encode())
paddy@112 382 req.Body = ioutil.NopCloser(body)
paddy@112 383 err = authCodeGrantInvalidate(req, testContext)
paddy@112 384 if err != nil {
paddy@112 385 t.Error("Error invalidating auth code:", err)
paddy@112 386 }
paddy@112 387 authCode, err := testContext.GetAuthorizationCode(code.Code)
paddy@112 388 if err != nil {
paddy@112 389 t.Error("Error retrieving auth code:", err)
paddy@112 390 }
paddy@112 391 if !authCode.Used {
paddy@112 392 t.Error("Expected auth code to be used, was not.")
paddy@112 393 }
paddy@112 394 }