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

Complexity requirements UI #579

Merged
merged 3 commits into from
Sep 18, 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
129 changes: 126 additions & 3 deletions cmd/server/assets/login/select-password.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,39 @@
<label for="retype">Retype password</label>
</div>

{{if .requirements.HasRequirements}}
<p class="card-text ml-4">
<small class="form-text text-muted">
<span class="row">Password should be:</span>
{{if gt .requirements.Length 0}}
<span class="row ml-1" id="length-req">
<span id="icon"></span>At least {{.requirements.Length}} characters long
</span>
{{end}}
{{if gt .requirements.Uppercase 0}}
<span class="row ml-1" id="upper-req">
<span id="icon"></span>Contain {{.requirements.Uppercase}} uppercase letter
</span>
{{end}}
{{if gt .requirements.Lowercase 0}}
<span class="row ml-1" id="lower-req">
<span id="icon"></span>Contain {{.requirements.Lowercase}} lowercase letter
</span>
{{end}}
{{if gt .requirements.Number 0}}
<span class="row ml-1" id="num-req">
<span id="icon"></span>Contain {{.requirements.Number}} number
</span>
{{end}}
{{if gt .requirements.Special 0}}
<span class="row ml-1" id="special-req">
<span id="icon"></span>Contain {{.requirements.Special}} special character
</span>
{{end}}
</small>
</p>
{{end}}

Copy link
Contributor

Choose a reason for hiding this comment

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

double checking - the password never gets sent to our sever, so JS validation is the last stop?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that's correct. It's not my favorite thing.

Copy link
Member

Choose a reason for hiding this comment

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

I think the password would go to us if we did server-side user creation though, right @whaught? The SDK just isn't ready for Go yet?

Copy link
Contributor

Choose a reason for hiding this comment

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

The existing FB admin sdk supports user creation and password setting

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No it's that the Go SDK doesn't support Multi-Factor-Auth and associated management.

I know we can create a user with a password, idk about changing it with an ooB code - I'll check

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The trouble is the oobCode that gets sent in the email - we have no way to verify that (outside of firebase) because we don't send the email.

If we had an SMTP server we could send the email ourselves with a token we track and verify server-side (then we could embed the validation logic server side too). We'd then need to handle oobCode storage and expiry ourselves too.

<button type="submit" id="submit" class="btn btn-primary btn-block">Set password</button>
</form>
</div>
Expand All @@ -59,10 +92,18 @@
let $password = $('#password');
let $retype = $('#retype');

{{if .requirements.HasRequirements}}
let $lenReq = $('#length-req');
let $upperReq = $('#upper-req');
let $lowerReq = $('#lower-req');
let $numReq = $('#num-req');
let $specialReq = $('#special-req');
{{end}}

let urlVars = getUrlVars();
let code = urlVars["oobCode"];
if (!code) {
code = ""
code = "";
}

firebase.auth().verifyPasswordResetCode(code)
Expand All @@ -71,7 +112,7 @@
}).catch(function(error) {
flash.error("Invalid password reset code. "
+ "The code may be malformed, expired, or has already been used.");
$submit.prop('disabled', true);
$submit.prop('disabled', true);
});

$form.on('submit', function(event) {
Expand All @@ -80,9 +121,91 @@
let email = $email.val();
let pwd = $password.val();
if (pwd != $retype.val()) {
flash("Password and retyped passwords must match.", "danger");
flash.error("Password and retyped passwords must match.");
return;
}

{{if .requirements.HasRequirements}}
let upper = 0;
let lower = 0;
let digit = 0;
let special = 0;
let specialPattern = new RegExp(/[~`!#$%\^&*+=\-\[\]\\';,/{}|\\":<>\?]/);
for (let i = 0; i < pwd.length; i++) {
let c = pwd.charAt(i);
if (!isNaN(parseInt(c, 10))) {
digit++;
} else if (specialPattern.test(c)) {
special++;
} else if (c == c.toUpperCase()) {
upper++;
} else if (c == c.toLowerCase()) {
lower++;
}
}

let fail = false;
let errClass = "oi oi-circle-x pr-1";
let checkClass = "oi oi-circle-check pr-1";

{{if gt .requirements.Length 0}}
if (pwd.length < {{.requirements.Length}}) {
$lenReq.find("#icon").attr("class", errClass)
$lenReq.addClass("text-danger");
fail = true;
} else {
$lenReq.find("#icon").attr("class", checkClass)
$lenReq.addClass("text-muted");
}
{{end}}

{{if gt .requirements.Uppercase 0}}
if (upper < {{.requirements.Uppercase}}) {
$upperReq.find("#icon").attr("class", errClass);
$upperReq.addClass("text-danger");
fail = true;
} else {
$upperReq.find("#icon").attr("class", checkClass);
$upperReq.addClass("text-muted");
}
{{end}}

{{if gt .requirements.Lowercase 0}}
if (lower < {{.requirements.Lowercase}}) {
$lowerReq.find("#icon").attr("class", errClass);
$lowerReq.addClass("text-danger");
fail = true;
} else {
$lowerReq.find("#icon").attr("class", checkClass);
$lowerReq.addClass("text-muted");
}
{{end}}

{{if gt .requirements.Number 0}}
if (digit < {{.requirements.Number}}) {
$numReq.find("#icon").attr("class", errClass);
$numReq.addClass("text-danger");
fail = true;
} else {
$numReq.find("#icon").attr("class", checkClass);
$numReq.addClass("text-muted");
}
{{end}}

{{if gt .requirements.Special 0}}
if (special < {{.requirements.Special}}) {
$specialReq.find("#icon").attr("class", errClass);
$specialReq.addClass("text-danger");
fail = true;
} else {
$specialReq.find("#icon").attr("class", checkClass);
$specialReq.addClass("text-muted");
}
if (fail) {
return;
}
{{end}}
{{end}}

// Disable the submit button so we only attempt once.
$submit.prop('disabled', true);
Expand Down
17 changes: 17 additions & 0 deletions pkg/config/server_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ import (

var _ IssueAPIConfig = (*ServerConfig)(nil)

// PasswordRequirementsConfig represents the password complexity requirements for the server.
type PasswordRequirementsConfig struct {
Length int `env:"MIN_PWD_LENGTH,default=8"`
Uppercase int `env:"MIN_PWD_UPPER,default=1"`
Lowercase int `env:"MIN_PWD_LOWER,default=1"`
Number int `env:"MIN_PWD_DIGITS,default=1"`
Special int `env:"MIN_PWD_SPECIAL,default=1"`
}

// HasRequirements is true if any requirments are set.
func (c *PasswordRequirementsConfig) HasRequirements() bool {
return c.Length > 0 || c.Uppercase > 0 || c.Lowercase > 0 || c.Number > 0 || c.Special > 0
}

// ServerConfig represents the environment based config for the server.
type ServerConfig struct {
Firebase FirebaseConfig
Expand All @@ -44,6 +58,9 @@ type ServerConfig struct {
SessionDuration time.Duration `env:"SESSION_DURATION, default=20h"`
RevokeCheckPeriod time.Duration `env:"REVOKE_CHECK_DURATION,default=5m"`

// Password Config
PasswordRequirements PasswordRequirementsConfig

// CookieKeys is a slice of bytes. The first is 64 bytes, the second is 32.
// They should be base64-encoded.
CookieKeys Base64ByteSlice `env:"COOKIE_KEYS,required"`
Expand Down
1 change: 1 addition & 0 deletions pkg/controller/login/select_password.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func (c *Controller) HandleSelectPassword() http.Handler {

m := controller.TemplateMapFromContext(ctx)
m["firebase"] = c.config.Firebase
m["requirements"] = &c.config.PasswordRequirements
c.h.RenderHTML(w, "login/select-password", m)
})
}