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

Commit

Permalink
Complexity requirements UI (#579)
Browse files Browse the repository at this point in the history
* complexity requirements UX

* server config

* semi-colons
  • Loading branch information
whaught authored Sep 18, 2020
1 parent 3ae74d7 commit a18dedf
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 3 deletions.
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}}

<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)
})
}

0 comments on commit a18dedf

Please sign in to comment.