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

Commit

Permalink
Email verification requirement setting (#563)
Browse files Browse the repository at this point in the history
* Email verification requirement setting

* remove importbatch

* undo formatting

* fixes

* rename type
  • Loading branch information
whaught authored Sep 17, 2020
1 parent cd8d744 commit 108c4c0
Show file tree
Hide file tree
Showing 13 changed files with 182 additions and 72 deletions.
9 changes: 5 additions & 4 deletions cmd/server/assets/login/register-phone.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@
<div class="card shadow-sm" id="register-div">
<div class="card-header">Multi-factor authentication</div>
<div class="card-body">
{{if eq .currentRealm.MFAModeString "required"}}
{{if eq .currentRealm.MFAMode.String "required"}}
<div class="alert alert-warning">
<span class="oi oi-warning"></span> This realm <strong>requires</strong> multi-factor authorization.
<span class="oi oi-warning"></span>
This realm <strong>requires</strong> multi-factor authorization.
</div>
{{end}}

<p>
<strong>{{$currentRealm.Name}}</strong>
{{if eq .currentRealm.MFAModeString "required"}}requires{{else}}recommends{{end}}
{{if eq .currentRealm.MFAMode.String "required"}}requires{{else}}recommends{{end}}
enhanced security via SMS-based 2-factor authentication. Please
provide your information below.
</p>
Expand All @@ -57,7 +58,7 @@
<button type="submit" id="submit-register" class="btn btn-primary btn-block">Register</button>
</form>

{{if ne .currentRealm.MFAModeString "required"}}
{{if ne .currentRealm.MFAMode.String "required"}}
<a id="skip" class="float-right mt-3 text-muted" href="/home">Skip for now</a>
{{end}}
</div>
Expand Down
51 changes: 32 additions & 19 deletions cmd/server/assets/login/verify-email.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,34 @@
<main role="main" class="container">
{{template "flash" .}}

<h1>Email verification</h1>
<p>
You must verify your email address to continue.
</p>

<div class="card mb-3 shadow-sm">
<div class="card-header">Email verification</div>
<div class="card-body">
<p>
Click the button below to send a verification email, then check your
inbox for the message.
</p>
<button id="verify" class="btn btn-primary btn-block" disabled>Send verification email</button>

<a id="skip" class="float-right mt-3 text-muted" href="/home">Skip for now</a>
<div class="d-flex vh-100">
<div class="d-flex w-100 justify-content-center">
<div class="col-sm-6">

<div class="card mb-3 shadow-sm">
<div class="card-header">Email verification</div>
<div class="card-body">
{{if eq .currentRealm.EmailVerifiedMode.String "required"}}
<div class="alert alert-warning">
<span class="oi oi-warning"></span>
This realm <strong>requires</strong> email address verification.
</div>
{{end}}

<p>Email address ownership for <em>{{.currentUser.Email}}</em> is <strong>not</strong> confirmed.
</p>

<button id="verify" class="btn btn-primary btn-block" disabled>Send verification email</button>

<small class="form-text text-muted">Click to send an email containing a verification
link.</small>

{{if ne .currentRealm.EmailVerifiedMode.String "required"}}
<a id="skip" class="float-right mt-3 text-muted" href="/home">Skip for now</a>
{{end}}
</div>
</div>
</div>
</div>
</div>
</main>
Expand All @@ -39,11 +52,11 @@ <h1>Email verification</h1>
let $verify = $('#verify');
let $skip = $('#skip');

$(function () {
$verify.on('click', function (event) {
$(function() {
$verify.on('click', function(event) {
let user = firebase.auth().currentUser
if (!user.emailVerified) {
user.sendEmailVerification().then(function () {
user.sendEmailVerification().then(function() {
clearExistingFlash();
flash("Verification email sent.", "success");
$verify.prop('disabled', true);
Expand All @@ -52,7 +65,7 @@ <h1>Email verification</h1>
});
});

firebase.auth().onAuthStateChanged(function (user) {
firebase.auth().onAuthStateChanged(function(user) {
if (!user) {
window.location.assign("/signout");
return;
Expand Down
17 changes: 14 additions & 3 deletions cmd/server/assets/realm.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,24 @@ <h1>Realm settings</h1>
</div>
</div>

<div class="form-group row">
<label for="emailVerifiedMode" class="col-sm-3">Email verified:</label>
<div class="col-sm-9">
<select name="emailVerifiedMode" id="emailVerifiedMode" class="form-control">
<option value="1" {{if eq $realm.EmailVerifiedMode.String "required"}}selected{{end}}>Required</option>
<option value="0" {{if eq $realm.EmailVerifiedMode.String "prompt"}}selected{{end}}>Prompt after login</option>
<option value="2" {{if eq $realm.EmailVerifiedMode.String "optional"}}selected{{end}}>Optional</option>
</select>
</div>
</div>

<div class="form-group row">
<label for="MFAMode" class="col-sm-3">Multi factor auth:</label>
<div class="col-sm-9">
<select name="MFAMode" id="MFAMode" class="form-control">
<option value=1 {{if eq $realm.MFAModeString "required"}}selected{{end}}>Required</option>
<option value=0 {{if eq $realm.MFAModeString "prompt"}}selected{{end}}>Prompt after login</option>
<option value=2 {{if eq $realm.MFAModeString "optional"}}selected{{end}}>Optional</option>
<option value="1" {{if eq $realm.MFAMode.String "required"}}selected{{end}}>Required</option>
<option value="0" {{if eq $realm.MFAMode.String "prompt"}}selected{{end}}>Prompt after login</option>
<option value="2" {{if eq $realm.MFAMode.String "optional"}}selected{{end}}>Optional</option>
</select>
</div>
</div>
Expand Down
22 changes: 11 additions & 11 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,20 +213,20 @@ func realMain(ctx context.Context) error {
sub.Handle("/session", loginController.HandleCreateSession()).Methods("POST")
sub.Handle("/signout", loginController.HandleSignOut()).Methods("GET")

// Verifying email requires the user is logged in
// Realm selection
sub = r.PathPrefix("").Subrouter()
sub.Use(requireAuth)
sub.Use(rateLimit)
sub.Use(loadCurrentRealm)
sub.Handle("/login/verify-email", loginController.HandleVerifyEmail()).Methods("GET")
sub.Handle("/login/select-realm", loginController.HandleSelectRealm()).Methods("GET", "POST")

// Realm selection
// Verifying email requires the user is logged in
sub = r.PathPrefix("").Subrouter()
sub.Use(requireAuth)
sub.Use(rateLimit)
sub.Use(loadCurrentRealm)
sub.Use(requireVerified)
sub.Handle("/login/select-realm", loginController.HandleSelectRealm()).Methods("GET", "POST")
sub.Use(requireRealm)
sub.Handle("/login/verify-email", loginController.HandleVerifyEmail()).Methods("GET")

// SMS auth registration is realm-specific, so it needs to load the current realm.
sub = r.PathPrefix("").Subrouter()
Expand All @@ -242,9 +242,9 @@ func realMain(ctx context.Context) error {
{
sub := r.PathPrefix("/home").Subrouter()
sub.Use(requireAuth)
sub.Use(requireVerified)
sub.Use(loadCurrentRealm)
sub.Use(requireRealm)
sub.Use(requireVerified)
sub.Use(requireMFA)
sub.Use(rateLimit)

Expand All @@ -262,9 +262,9 @@ func realMain(ctx context.Context) error {
{
sub := r.PathPrefix("/code").Subrouter()
sub.Use(requireAuth)
sub.Use(requireVerified)
sub.Use(loadCurrentRealm)
sub.Use(requireRealm)
sub.Use(requireVerified)
sub.Use(requireMFA)
sub.Use(rateLimit)

Expand All @@ -278,9 +278,9 @@ func realMain(ctx context.Context) error {
{
sub := r.PathPrefix("/apikeys").Subrouter()
sub.Use(requireAuth)
sub.Use(requireVerified)
sub.Use(loadCurrentRealm)
sub.Use(requireAdmin)
sub.Use(requireVerified)
sub.Use(requireMFA)
sub.Use(rateLimit)

Expand All @@ -299,9 +299,9 @@ func realMain(ctx context.Context) error {
{
userSub := r.PathPrefix("/users").Subrouter()
userSub.Use(requireAuth)
userSub.Use(requireVerified)
userSub.Use(loadCurrentRealm)
userSub.Use(requireAdmin)
userSub.Use(requireVerified)
userSub.Use(requireMFA)
userSub.Use(rateLimit)

Expand All @@ -321,9 +321,9 @@ func realMain(ctx context.Context) error {
{
realmSub := r.PathPrefix("/realm").Subrouter()
realmSub.Use(requireAuth)
realmSub.Use(requireVerified)
realmSub.Use(loadCurrentRealm)
realmSub.Use(requireAdmin)
realmSub.Use(requireVerified)
realmSub.Use(requireMFA)
realmSub.Use(rateLimit)

Expand Down Expand Up @@ -362,8 +362,8 @@ func realMain(ctx context.Context) error {
{
adminSub := r.PathPrefix("/admin").Subrouter()
adminSub.Use(requireAuth)
adminSub.Use(requireVerified)
adminSub.Use(loadCurrentRealm)
adminSub.Use(requireVerified)
adminSub.Use(requireSystemAdmin)
adminSub.Use(rateLimit)

Expand Down
21 changes: 21 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,27 @@ type CSRFResponse struct {
ErrorCode string `json:"errorCode"`
}

// UserBatchRequest is a request for bulk creation of users.
// This is called by the Web frontend.
// API is served at /users/import/userbatch
type UserBatchRequest struct {
Users []BatchUser `json:"users"`
}

// BatchUser represents a single user's email/name.
type BatchUser struct {
Email string `json:"email"`
Name string `json:"name"`
}

// UserBatchResponse defines the response type for UserBatchRequest.
type UserBatchResponse struct {
NewUsers []*BatchUser `json:"newUsers"`

Error string `json:"error"`
ErrorCode string `json:"errorCode,omitempty"`
}

// IssueCodeRequest defines the parameters to request an new OTP (short term)
// code. This is called by the Web frontend.
// API is served at /api/issue
Expand Down
6 changes: 0 additions & 6 deletions pkg/controller/login/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@ func (c *Controller) HandleRegisterPhone() http.Handler {
return
}

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

// Mark prompted so we only prompt once.
controller.StoreSessionMFAPrompted(session, true)

Expand Down
9 changes: 9 additions & 0 deletions pkg/controller/login/verify_email.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ func (c *Controller) HandleVerifyEmail() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

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

// Mark prompted so we only prompt once.
controller.StoreSessionEmailVerificationPrompted(session, true)

m := controller.TemplateMapFromContext(ctx)
m["firebase"] = c.config.Firebase
c.h.RenderHTML(w, "login/verify-email", m)
Expand Down
21 changes: 18 additions & 3 deletions pkg/controller/middleware/emailverified.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

"firebase.google.com/go/auth"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
)

// RequireVerified requires a user to have verified their login email.
Expand Down Expand Up @@ -68,10 +69,10 @@ func RequireVerified(ctx context.Context, client *auth.Client, db *database.Data
controller.Unauthorized(w, r, h)
return
}
if !fbUser.EmailVerified {
delete(m, "currentUser") // Remove user from the template map.

realm := controller.RealmFromContext(ctx)
if NeedsEmailVerification(session, realm, fbUser) {
logger.Debugw("user email not verified")
flash.Error("User email not verified.")
http.Redirect(w, r, "/login/verify-email", http.StatusSeeOther)
return
}
Expand All @@ -80,3 +81,17 @@ func RequireVerified(ctx context.Context, client *auth.Client, db *database.Data
})
}
}

func NeedsEmailVerification(session *sessions.Session, realm *database.Realm, fbUser *auth.UserRecord) bool {
if (realm == nil || realm.EmailVerifiedMode == database.MFARequired) && !fbUser.EmailVerified {
return true
}

if realm.EmailVerifiedMode == database.MFAOptionalPrompt &&
!controller.EmailVerificationPromptedFromSession(session) &&
!fbUser.EmailVerified {
return true
}

return false
}
14 changes: 8 additions & 6 deletions pkg/controller/realmadmin/save.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ func init() {

func (c *Controller) HandleSave() http.Handler {
type FormData struct {
Name string `form:"name"`
RegionCode string `form:"regionCode"`
AllowedTestTypes database.TestType `form:"allowedTestTypes"`
MFAMode int16 `form:"MFAMode"`
RequireDate bool `form:"requireDate"`
Name string `form:"name"`
RegionCode string `form:"regionCode"`
AllowedTestTypes database.TestType `form:"allowedTestTypes"`
MFAMode int16 `form:"MFAMode"`
EmailVerifiedMode int16 `form:"emailVerifiedMode"`
RequireDate bool `form:"requireDate"`

CodeLength uint `form:"codeLength"`
CodeDurationMinutes int64 `form:"codeDuration"`
Expand Down Expand Up @@ -106,7 +107,8 @@ func (c *Controller) HandleSave() http.Handler {
realm.LongCodeLength = form.LongCodeLength
realm.LongCodeDuration.Duration = time.Hour * time.Duration(form.LongCodeHours)
realm.SMSTextTemplate = form.SMSTextTemplate
realm.MFAMode = database.MFAMode(form.MFAMode)
realm.MFAMode = database.AuthRequirement(form.MFAMode)
realm.EmailVerifiedMode = database.AuthRequirement(form.EmailVerifiedMode)
realm.AbusePreventionEnabled = form.AbusePreventionEnabled
realm.AbusePreventionLimitFactor = form.AbusePreventionLimitFactor
if err := c.db.SaveRealm(realm); err != nil {
Expand Down
Loading

0 comments on commit 108c4c0

Please sign in to comment.