func changeAccountPassword(ctx context.Context, w http.ResponseWriter, r *http.Request) { var acct account.Account var email string var newPassword string // read the account ID if emailBytes, err := base64.RawURLEncoding.DecodeString(rest.Param(ctx, "id")); err != nil { rest.WriteJSON(w, err) return } else { email = string(emailBytes) } if err := rest.ReadJSON(r, &newPassword); err != nil { rest.WriteJSON(w, err) } else if err := account.Get(ctx, email, &acct); err != nil { rest.WriteJSON(w, err) } else if err := acct.SetPassword(newPassword); err != nil { rest.WriteJSON(w, err) } else if err := account.Save(ctx, &acct); err != nil { rest.WriteJSON(w, err) } else { rest.WriteJSON(w, &rest.NoContent) } }
// HasRole returns an AuthCheck that grants access under the following conditions: // - The account specified by the token has the specified role. // - The token itself has that role in scope. // - The token is not used up. func HasRole(role string) AuthCheck { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { var acct account.Account var claimSet jws.ClaimSet if err := GetAccount(ctx, &acct); err != nil { // we can't get the user, so we can't check authentication status. log.Errorf(ctx, "Error getting account for authentication: %s", err.Error()) return ErrCannotGetAccount } else if err := GetClaimSet(ctx, &claimSet); err != nil { log.Errorf(ctx, "Error getting claim set for authentication: %s", err.Error()) return ErrCannotGetClaimSet } else if !acct.HasRole(role) { // the account making the request does not have the specified role. return ErrRoleMissing } else if !roleInScope(ctx, role) { // the JWT claimset for this request does not have the specified role in its scope. return ErrRoleNotInScope } else if err = UseClaimSet(ctx, &claimSet); err == ErrClaimSetUsedUp { // The claimset for this request has been all used up. return err } else if err != nil { return errors.New(http.StatusBadRequest, err.Error()) } else { // All ok; this request's account may access the resource. return nil } } }
// Super is an AuthCheck that grants access to the superuser (that is, the user // identified by `Authorization: <secret>`). func Super(ctx context.Context, w http.ResponseWriter, r *http.Request) error { var acct account.Account if err := GetAccount(ctx, &acct); err != nil { return err } else if !acct.Super() { return ErrForbidden } else { return nil } }
// AccountMatchesParam returns an AuthCheck that grants access if paramName is the same // as the account's ID; so, for instance, on a route to /accounts/:accountId, with // a request to /accounts/asdf, the AuthCheck will return true if the account's ID is asdf. // As a special case, account.Nobody and account.Super will never match in this method. func AccountMatchesParam(paramName string) AuthCheck { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { var acct account.Account if err := GetAccount(ctx, &acct); err != nil { return err } else if acct.Super() || acct.Nobody() { return ErrAccountIDDoesNotMatch } else if acct.Key(ctx).Encode() != kami.Param(ctx, paramName) { return ErrAccountIDDoesNotMatch } else { return nil } } }
func TestMiddleware(t *testing.T) { var acct account.Account ctx := test.WithConfig(context.Background(), map[string]interface{}{"AuthSecret": "foo"}) // a user is no one (i.e., no Authorization header) w := httptest.NewRecorder() r, _ := http.NewRequest("GET", "/", nil) resultCtx := Middleware(ctx, w, r) err := GetAccount(resultCtx, &acct) if err != nil { t.Errorf("Unexpected error %s", err) } if !acct.Nobody() { t.Errorf("acct should have been the zero value, but got %+v", acct) } // bad jwt w = httptest.NewRecorder() r, _ = http.NewRequest("GET", "/", nil) r.Header.Set("Authorization", "wrong") resultCtx = Middleware(ctx, w, r) err = GetAccount(resultCtx, &acct) if err != InvalidJWTError { t.Errorf("Unexpected error %s", err) } // super (i.e., they passed in the secret itself) w = httptest.NewRecorder() r, _ = http.NewRequest("GET", "/", nil) r.Header.Set("Authorization", "foo") resultCtx = Middleware(ctx, w, r) err = GetAccount(resultCtx, &acct) if err != nil { t.Errorf("Unexpected error %s", err) } if !acct.Super() { t.Errorf("Expected the super account, but got %+v", acct) } // super (i.e., they passed in the secret itself) w = httptest.NewRecorder() r, _ = http.NewRequest("GET", "/", nil) r.Header.Set("Authorization", "foo") resultCtx = Middleware(ctx, w, r) err = GetAccount(resultCtx, &acct) if err != nil { t.Errorf("Unexpected error %s", err) } if !acct.Super() { t.Errorf("Expected the super account, but got %+v", acct) } // test against App Engine dev environment from this point forward realCtx, done, _ := aetest.NewContext() defer done() account.New(realCtx, "*****@*****.**", "foobar") realCtx = test.WithConfig(realCtx, map[string]interface{}{"AuthSecret": "foo"}) // nonexistent account w = httptest.NewRecorder() r, _ = http.NewRequest("GET", "/", nil) r.Header.Set("Authorization", test.JWT(&jws.ClaimSet{Sub: "*****@*****.**"}, "foo")) resultCtx = Middleware(realCtx, w, r) if resultCtx != nil { t.Errorf("Expected Middleware to terminate (i.e. to return nil), but it didn't", err) } // existent account (i.e., the happy path) w = httptest.NewRecorder() r, _ = http.NewRequest("GET", "/", nil) r.Header.Set("Authorization", test.JWT(&jws.ClaimSet{Sub: "*****@*****.**"}, "foo")) resultCtx = Middleware(realCtx, w, r) if resultCtx == nil { t.Errorf("Expected Middleware not to terminate (i.e. to return another context), but it terminated") } else if err = GetAccount(resultCtx, &acct); err != nil { t.Errorf("Expected no error when getting a properly authenticated account, but got %s", err) } else if acct.Email != "*****@*****.**" { t.Errorf("Unexpected account on retrieval: %+v", acct) } }