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

Create a grace period before MFA is required #723

Merged
merged 6 commits into from
Oct 1, 2020
Merged
Show file tree
Hide file tree
Changes from 5 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
32 changes: 32 additions & 0 deletions cmd/server/assets/realmadmin/_form_security.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@
</small>
</div>

<div class="form-group">
<label for="mfa-grace-period">Require MFA</label>
<select name="mfa_grace_period" id="mfa-grace-period" class="form-control custom-select" {{if ne $realm.MFAMode.String "required"}}disabled{{end}}>
{{$current := $realm.MFARequiredGracePeriod}}
{{range $prd := .mfaGracePeriod}}
<option value="{{$prd}}" {{if eq $prd $current.Days}}selected{{end}}>
{{if (eq $prd 0)}}Immediately{{else}}After {{$prd}} days{{end}}
</option>
{{end}}
</select>
{{template "errorable" $realm.ErrorsFor "mfaMode"}}
<small class="form-text text-muted">
Allows users to continue without registering multi-factor authentication for a period of time. This can be
less burdensome to new users or short-term users.
</small>
</div>

<div class="form-group">
<label for="password-rotation-period-days">Require password rotation</label>
<select name="password_rotation_period_days" id="password-rotation-period-days" class="form-control custom-select">
Expand Down Expand Up @@ -123,4 +140,19 @@
</div>
</form>

<script type="text/javascript">
$(function(){
let $grace = $('#mfa-grace-period');
let $mfaMode = $('#mfa-mode');

$mfaMode.change(function() {
if ($mfaMode.val() == 1) {
$grace.prop("disabled", false);
whaught marked this conversation as resolved.
Show resolved Hide resolved
} else {
$grace.prop("disabled", true);
}
});
});
</script>

{{end}}
15 changes: 13 additions & 2 deletions pkg/controller/middleware/mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package middleware
import (
"context"
"net/http"
"time"

"github.com/google/exposure-notifications-verification-server/pkg/controller"
"github.com/google/exposure-notifications-verification-server/pkg/database"
Expand All @@ -40,8 +41,14 @@ func RequireMFA(ctx context.Context, h *render.Renderer) mux.MiddlewareFunc {
return
}

currentUser := controller.UserFromContext(ctx)
if currentUser == nil {
controller.MissingUser(w, r, h)
return
}

realm := controller.RealmFromContext(ctx)
if NeedsMFARedirect(session, realm) {
if NeedsMFARedirect(session, currentUser, realm) {
controller.RedirectToMFA(w, r, h)
return
}
Expand All @@ -51,8 +58,12 @@ func RequireMFA(ctx context.Context, h *render.Renderer) mux.MiddlewareFunc {
}
}

func NeedsMFARedirect(session *sessions.Session, realm *database.Realm) bool {
func NeedsMFARedirect(session *sessions.Session, user *database.User, realm *database.Realm) bool {
if (realm == nil || realm.MFAMode == database.MFARequired) && controller.FactorCountFromSession(session) == 0 {
if time.Since(user.CreatedAt) <= realm.MFARequiredGracePeriod.Duration && controller.MFAPromptedFromSession(session) {
return false
}

return true
}

Expand Down
6 changes: 5 additions & 1 deletion pkg/controller/realmadmin/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var (
shortCodeMinutes = []int{}
longCodeLengths = []int{12, 13, 14, 15, 16}
longCodeHours = []int{}
mfaGracePeriod = []int64{0, 1, 7, 30}
passwordRotationPeriodDays = []int{0, 30, 60, 90, 365}
passwordRotationWarningDays = []int{0, 1, 3, 5, 7, 30}
)
Expand Down Expand Up @@ -69,6 +70,7 @@ func (c *Controller) HandleSettings() http.Handler {

Security bool `form:"security"`
MFAMode int16 `form:"mfa_mode"`
MFARequiredGracePeriod int64 `form:"mfa_grace_period"`
EmailVerifiedMode int16 `form:"email_verified_mode"`
PasswordRotationPeriodDays uint `form:"password_rotation_period_days"`
PasswordRotationWarningDays uint `form:"password_rotation_warning_days"`
Expand Down Expand Up @@ -148,6 +150,7 @@ func (c *Controller) HandleSettings() http.Handler {
if form.Security {
realm.EmailVerifiedMode = database.AuthRequirement(form.EmailVerifiedMode)
realm.MFAMode = database.AuthRequirement(form.MFAMode)
realm.MFARequiredGracePeriod = database.FromDuration(time.Duration(form.MFARequiredGracePeriod) * 24 * time.Hour)
realm.PasswordRotationPeriodDays = form.PasswordRotationPeriodDays
realm.PasswordRotationWarningDays = form.PasswordRotationWarningDays

Expand Down Expand Up @@ -267,7 +270,7 @@ func (c *Controller) renderSettings(ctx context.Context, w http.ResponseWriter,
}

// Don't pass through the system config to the template - we don't want to
// risk accidentially rendering its ID or values since the realm should never
// risk accidentally rendering its ID or values since the realm should never
// see these values. However, we have to go lookup the actual SMS config
// values if present so that if the user unchecks the form, they don't see
// blank values if they were previously using their own SMS configs.
Expand Down Expand Up @@ -297,6 +300,7 @@ func (c *Controller) renderSettings(ctx context.Context, w http.ResponseWriter,
"negative": database.TestTypeConfirmed | database.TestTypeLikely | database.TestTypeNegative,
}
// Valid settings for pwd rotation.
m["mfaGracePeriod"] = mfaGracePeriod
m["passwordRotateDays"] = passwordRotationPeriodDays
m["passwordWarnDays"] = passwordRotationWarningDays
// Valid settings for code parameters.
Expand Down
6 changes: 5 additions & 1 deletion pkg/database/duration.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var _ driver.Valuer = (*DurationSeconds)(nil)
type DurationSeconds struct {
Duration time.Duration

// AsString allows this value to be updatd and parsed using the Update() method.
// AsString allows this value to be updated and parsed using the Update() method.
AsString string
}

Expand All @@ -40,6 +40,10 @@ func FromDuration(d time.Duration) DurationSeconds {
}
}

func (d *DurationSeconds) Days() int64 {
return int64(d.Duration.Hours() / 24)
whaught marked this conversation as resolved.
Show resolved Hide resolved
}

// Update attempts to parse the AsString value and set is as the duration
func (d *DurationSeconds) Update() error {
newDuration, err := time.ParseDuration(d.AsString)
Expand Down
10 changes: 10 additions & 0 deletions pkg/database/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -1417,6 +1417,16 @@ func (db *Database) getMigrations(ctx context.Context) *gormigrate.Gormigrate {
return nil
},
},
{
ID: "00057-AddMFARequiredGracePeriod",
Migrate: func(tx *gorm.DB) error {
logger.Debugw("adding email verification required to realm")
return tx.Exec("ALTER TABLE realms ADD COLUMN IF NOT EXISTS mfa_required_grace_period BIGINT DEFAULT 0").Error
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the default be 0 or leave it as nullable?

},
Rollback: func(tx *gorm.DB) error {
return tx.Exec("ALTER TABLE realms DROP COLUMN IF EXISTS mfa_required_grace_period").Error
},
},
})
}

Expand Down
10 changes: 10 additions & 0 deletions pkg/database/realm.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ type Realm struct {
// MFAMode represents the mode for Multi-Factor-Authorization requirements for the realm.
MFAMode AuthRequirement `gorm:"type:smallint; not null; default: 0"`

// MFARequiredGracePeriod defines how long after creation a user may skip adding
// a second auth factor before the server requires it.
MFARequiredGracePeriod DurationSeconds `gorm:"type:bigint; not null; default: 0"`

// EmailVerifiedMode represents the mode for email verification requirements for the realm.
EmailVerifiedMode AuthRequirement `gorm:"type:smallint; not null; default: 0"`

Expand Down Expand Up @@ -841,6 +845,12 @@ func (db *Database) SaveRealm(r *Realm, actor Auditable) error {
audits = append(audits, audit)
}

if existing.MFARequiredGracePeriod != r.MFARequiredGracePeriod {
audit := BuildAuditEntry(actor, "updated MFA required grace period", r, r.ID)
audit.Diff = stringDiff(existing.MFARequiredGracePeriod.AsString, r.MFARequiredGracePeriod.AsString)
audits = append(audits, audit)
}

if existing.EmailVerifiedMode != r.EmailVerifiedMode {
audit := BuildAuditEntry(actor, "updated email verification mode", r, r.ID)
audit.Diff = stringDiff(existing.EmailVerifiedMode.String(), r.EmailVerifiedMode.String())
Expand Down