Skip to content
This repository has been archived by the owner on Jul 12, 2023. It is now read-only.

Recreate firebase user on password reset #685

Closed
wants to merge 19 commits into from
Closed
36 changes: 6 additions & 30 deletions cmd/server/assets/users/show.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ <h1>{{$user.Name}}</h1>
{{$user.CanAdminRealm $currentRealm.ID}}
</div>

<button id="password-reset" class="btn btn-primary btn-block disabled">
Send password reset
</button>

<form class="floating-form" action="/users/{{$user.ID}}" method="POST">
{{ .csrfField }}
<input type="hidden" name="email" value="{{$user.Email}}"/>
<button type="password-reset" id="submit" class="btn btn-primary btn-block">Send password reset</button>
</form>
</div>
</div>

Expand Down Expand Up @@ -111,33 +114,6 @@ <h1>{{$user.Name}}</h1>
}
</script>
{{end}}

<script type="text/javascript">
$(function() {
$resetPassword = $('#password-reset');
$resetPassword.removeClass('disabled');

$resetPassword.on('click', function(event) {
event.preventDefault();
sendReset(function() {
flash.alert(`Password reset email sent.`);
});
});

function sendReset(successFn) {
$resetPassword.addClass('disabled');
let email = {{$user.Email}};
return firebase.auth().sendPasswordResetEmail(email)
.then(function() {
setTimeout($resetPassword.addClass('disabled'), 10000);
successFn();
}).catch(function(error) {
$resetPassword.removeClass('disabled');
flash.error(error.message);
});
}
});
</script>
</body>
</html>
{{end}}
1 change: 1 addition & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ func realMain(ctx context.Context) error {
userSub.Handle("/import", userController.HandleImportBatch()).Methods("POST")
userSub.Handle("/{id}/edit", userController.HandleUpdate()).Methods("GET")
userSub.Handle("/{id}", userController.HandleShow()).Methods("GET")
userSub.Handle("/{id}", userController.HandleResetPassword()).Methods("POST")
userSub.Handle("/{id}", userController.HandleUpdate()).Methods("PATCH")
userSub.Handle("/{id}", userController.HandleDelete()).Methods("DELETE")
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/controller/login/reset_password.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ func (c *Controller) HandleSubmitResetPassword() http.Handler {
return
}

// Ensure that if we have a user, they have auth
if user, err := c.db.FindUserByEmail(form.Email); err == nil {
user.CreateFirebaseUser(ctx, c.client)
whaught marked this conversation as resolved.
Show resolved Hide resolved
}

if err := c.firebaseInternal.SendPasswordResetEmail(ctx, form.Email); err != nil {
// Treat not-found like success so we don't leak details.
if !errors.Is(err, firebase.ErrEmailNotFound) {
Expand Down
9 changes: 1 addition & 8 deletions pkg/controller/user/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,9 @@ func (c *Controller) HandleCreate() http.Handler {
}

// Create firebase user first, if this fails we don't want a db.User entry
if created, err := user.CreateFirebaseUser(ctx, c.client); err != nil {
flash.Alert("Failed to create user: %v", err)
if _, err := c.createFirebaseUser(ctx, user); err != nil {
c.renderNew(ctx, w)
return
} else if created {
if err := c.firebaseInternal.SendPasswordResetEmail(ctx, user.Email); err != nil {
flash.Error("Failed sending new user invitation: %v", err)
c.renderNew(ctx, w)
return
}
}

// Build the user struct - keeping email and name if user already exists in another realm.
Expand Down
58 changes: 58 additions & 0 deletions pkg/controller/user/reset_password.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package user

import (
"context"
"net/http"

"github.com/google/exposure-notifications-verification-server/pkg/controller"
"github.com/google/exposure-notifications-verification-server/pkg/database"
)

func (c *Controller) HandleResetPassword() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Show(w, r, true)
})
}

func (c *Controller) resetPassword(ctx context.Context, user *database.User) (bool, error) {
return c.maybeResetPassword(ctx, true, user)
}

func (c *Controller) createFirebaseUser(ctx context.Context, user *database.User) (bool, error) {
return c.maybeResetPassword(ctx, false, user)
}

func (c *Controller) maybeResetPassword(ctx context.Context, reset bool, user *database.User) (bool, error) {
session := controller.SessionFromContext(ctx)
flash := controller.Flash(session)

// Ensure the firebase user is created
created, err := user.CreateFirebaseUser(ctx, c.client)
if err != nil {
flash.Alert("Failed to create user auth: %v", err)
return created, err
}

// Reset if we just created or a reset was asked for.
if created || reset {
if err := c.firebaseInternal.SendPasswordResetEmail(ctx, user.Email); err != nil {
flash.Error("Failed sending new user invitation: %v", err)
return true, err
}
}
return created, nil
}
63 changes: 37 additions & 26 deletions pkg/controller/user/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,41 +27,52 @@ import (

func (c *Controller) HandleShow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
c.Show(w, r, false)
})
}

session := controller.SessionFromContext(ctx)
if session == nil {
controller.MissingSession(w, r, c.h)
return
}
func (c *Controller) Show(w http.ResponseWriter, r *http.Request, resetPassword bool) {
ctx := r.Context()
vars := mux.Vars(r)

realm := controller.RealmFromContext(ctx)
if realm == nil {
controller.MissingRealm(w, r, c.h)
return
}
session := controller.SessionFromContext(ctx)
if session == nil {
controller.MissingSession(w, r, c.h)
return
}
flash := controller.Flash(session)

// Pull the user from the id.
user, err := realm.FindUser(c.db, vars["id"])
if err != nil {
if database.IsNotFound(err) {
controller.Unauthorized(w, r, c.h)
return
}
realm := controller.RealmFromContext(ctx)
if realm == nil {
controller.MissingRealm(w, r, c.h)
return
}

controller.InternalError(w, r, c.h, err)
// Pull the user from the id.
user, err := realm.FindUser(c.db, vars["id"])
if err != nil {
if database.IsNotFound(err) {
controller.Unauthorized(w, r, c.h)
return
}

stats, err := c.getStats(ctx, user, realm)
if err != nil {
controller.InternalError(w, r, c.h, err)
return
controller.InternalError(w, r, c.h, err)
return
}

if resetPassword {
if _, err := c.resetPassword(ctx, user); err == nil {
flash.Alert("Password reset email sent.")
}
}

c.renderShow(ctx, w, user, stats)
})
stats, err := c.getStats(ctx, user, realm)
if err != nil {
controller.InternalError(w, r, c.h, err)
return
}

c.renderShow(ctx, w, user, stats)
}

// Get and cache the stats for this user.
Expand Down