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

Email verification requirement setting #563

Merged
merged 6 commits into from
Sep 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -214,20 +214,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 @@ -243,9 +243,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 @@ -263,9 +263,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 @@ -279,9 +279,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 @@ -300,9 +300,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 @@ -322,9 +322,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 @@ -363,8 +363,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