Building a custom Angular password strength validator
Password fields are where security policy meets everyday frustration. A form that rejects unfamiliar passphrases encourages weak reuse, while a green meter without explanation gives false confidence. A custom Angular validator can combine a predictable score with immediate, accessible feedback. The browser is only the first checkpoint, though: clients can be changed, bypassed or automated.
This distinction matters for Australian products. People commonly manage banking, superannuation, health and government accounts on mobile devices, often checking small details while commuting on trains in Sydney or Melbourne. Password managers and passkeys help, but they do not remove the need for a sensible fallback. A local validator can encourage a long, unique value without forcing users through a brittle sequence of capital letters, digits and symbols.
The implementation below uses Angular's typed reactive forms and separates reusable scoring from the validator attached to a control. It provides unit-testable logic and feedback suitable for keyboard and screen-reader users. The same rules still need to be enforced by the API, stored safely and monitored without recording the secret itself. That end-to-end approach suits Australian businesses operating under the Privacy Act 1988 and the Australian Privacy Principles, as well as international services with Australian customers.
Set a password policy before writing code
Start with a policy the security team, product owner and support team can explain in one sentence. Current Australian Signals Directorate guidance favours long, unique passphrases, multi-factor authentication and passkeys rather than routine password expiry or complicated composition rules. A practical web policy can require 14 characters, reject values already known to be breached, permit password managers and avoid silently rewriting what the user enters.
Australian customers use the same password field for very different risks. A Melbourne clinic, a Brisbane payroll platform and a national retailer may each have different obligations, yet all should avoid storing raw passwords or collecting unnecessary identity data. Under the Privacy Act 1988, account and recovery information may be personal information, and the Australian Privacy Principles require collection and security to be relevant, limited and protected. A password validator is only one part of that broader design.
Write down what constitutes weak, acceptable and strong before implementation. Length should carry most of the weight because it increases the work an attacker must do, while predictable strings such as password123 or 111111 should never receive a respectable score. Avoid rules that reject a long passphrase simply because it contains spaces. If a user can paste a generated value from a password manager, the policy should accept it.
Build the validator as a testable Angular function
In Angular, a validator is a function that receives an AbstractControl and returns ValidationErrors or null. Returning null means the value is acceptable; returning an object marks the control invalid. Keep the scoring logic independent from the form so it can be tested directly and reused for registration and password-change screens. This separation also prevents a template from becoming the source of security truth.
The following baseline is deliberately transparent. It is a policy-aware indicator, not a claim of cryptographic entropy: production services can add a vetted dictionary or breached-password check without changing the component contract.
import {
AbstractControl,
ValidationErrors,
ValidatorFn,
} from '@angular/forms';
export interface PasswordAssessment {
score: number;
label: string;
issues: string[];
}
const labels: Record<number, string> = [
'Very weak',
'Weak',
'Good',
'Strong',
'Very strong',
];
export function assessPassword(password: string): PasswordAssessment {
const length = Array.from(password).length;
const issues: string[] = [];
if (length > 0 && length < 14) {
issues.push('Use at least 14 characters.');
}
const predictable =
/^(password|passphrase|qwerty|letmein|admin)/i.test(password) ||
/(0123|1234|2345|3456|4567|5678|6789)/.test(password) ||
/(.)\1{7,}/.test(password);
if (predictable) {
issues.push(
'Avoid common words, number runs and repeated characters.',
);
}
let score = length >= 20 ? 4
: length >= 16 ? 3
: length >= 14 ? 2
: length >= 10 ? 1
: 0;
if (predictable) score = Math.min(score, 1);
return { score, label: labels[score], issues };
}
export function passwordStrengthValidator(
minimumScore = 2,
): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const password = control.value;
if (typeof password !== 'string' || password.length === 0) {
return null;
}
const assessment = assessPassword(password);
return assessment.score < minimumScore
? { passwordStrength: assessment }
: null;
};
}
The validator deliberately leaves an empty control alone so Validators.required can report the missing value. It never trims, lowercases or otherwise rewrites the secret. The minimum score is a policy parameter, and the predictable-pattern rule caps a superficially long value. In a larger application, run dictionary matching on the server as well, keep password processing out of analytics and review any external checking service as a third-party data flow requiring a documented privacy assessment.
Connect the score to an accessible reactive form
The score is not enough on its own. Every state needs a text label, and colour must be supplementary because users with low vision or colour-vision differences may not perceive the change. Announce changes politely, keep the instructions close to the input, and never use placeholder text as the only label. Delay any error announcement until the field has been touched so a new form is not announced as invalid immediately.
A typed reactive form can keep the latest assessment in a signal while the validator remains the source of form validity. This component uses Angular's current control-flow syntax; the same validator also works with an older *ngIf and *ngFor template if the project has not migrated.
import { Component, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import {
assessPassword,
passwordStrengthValidator,
} from './password-strength.validator';
@Component({
selector: 'app-password-field',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './password-field.component.html',
})
export class PasswordFieldComponent {
readonly form = new FormGroup({
password: new FormControl('', {
nonNullable: true,
validators: [Validators.required, passwordStrengthValidator(2)],
}),
});
readonly strength = signal(assessPassword(''));
constructor() {
this.form.controls.password.valueChanges
.pipe(takeUntilDestroyed())
.subscribe((password) =>
this.strength.set(assessPassword(password)),
);
}
}
<label for="password">Password</label>
<input
id="password"
type="password"
formControlName="password"
autocomplete="new-password"
aria-describedby="password-help password-status"
[attr.aria-invalid]="
form.controls.password.invalid && form.controls.password.touched
? 'true'
: null
"
/>
<p id="password-help">
Use at least 14 characters. A long passphrase is fine.
</p>
<div id="password-status" aria-live="polite">
@if (form.controls.password.dirty) {
<p>Strength: {{ strength().label }}</p>
@for (issue of strength().issues; track issue) {
<p>{{ issue }}</p>
}
}
</div>
Ask for confirmation only when it prevents a troublesome typo; it should not replace guidance on the first field. On smaller screens, keep the meter close to the input and ensure the page can scroll without hiding the field or its error message.
Before choosing labels, colours and timing, Aaron Orosen's password strength patterns can provide a useful interaction reference. Adapt any pattern to the product's design system rather than copying it mechanically. For a registration form, show progress only after the user starts typing and provide a submit button that explains failure instead of mysteriously doing nothing.
Separate server rules from browser guidance
A browser validator improves guidance, but it cannot protect the account. Anyone can call the API directly with a weak value, so the server owns enforcement and the browser owns feedback. This split also gives security teams one authoritative policy and lets product teams improve the experience without weakening the API.
Rules for the server
- Enforce the minimum length and common-password denylist in the API, not only in Angular.
- Hash accepted passwords with a memory-hard function such as Argon2id, using a unique salt and suitable work factors.
- Rate-limit authentication attempts and protect reset flows; a strong-password rule is no substitute for abuse controls.
- Never place raw passwords, full hashes or reset secrets in application logs, analytics events or error traces.
Choices for the browser
- Show a plain-language strength label alongside any meter, with readable contrast and a non-colour cue.
- Give immediate feedback after typing while keeping final form errors concise and associated with the relevant field.
- Allow paste and password-manager autofill, and do not block uncommon but strong passphrases with rigid composition rules.
- Offer multi-factor authentication or passkeys where the risk and account type justify them.
The lists reflect a useful division of responsibility. The API can perform expensive dictionary or breach checks, while the browser should remain responsive. A team should also avoid exposing which exact rule triggered a server rejection because that can help someone refine an attack. Return a safe general message, log enough metadata to investigate abuse and retain no secret material.
Test the behaviour and make the rules enforceable
Treat the scorer and validator as domain logic that deserves automated coverage. Test the boundaries, common values, predictable strings and long passphrases, then add component tests for labels, keyboard use and screen-reader text. Avoid asserting that one example is mathematically secure; assert that your agreed policy produces the expected score.
import { FormControl } from '@angular/forms';
import {
assessPassword,
passwordStrengthValidator,
} from './password-strength.validator';
describe('passwordStrengthValidator', () => {
it('rejects a common value', () => {
expect(assessPassword('password123').score).toBe(1);
});
it('rejects repeated characters', () => {
expect(assessPassword('aaaaaaaaaaaaaa').score).toBe(1);
});
it('accepts a long unrelated passphrase', () => {
const control = new FormControl('river-otter-bicycle-47');
expect(passwordStrengthValidator()(control)).toBeNull();
});
it('leaves an empty value to the required validator', () => {
expect(
passwordStrengthValidator()(new FormControl('')),
).toBeNull();
});
});
The server should use the same minimum and rejection policy, then return structured validation errors that the Angular form can map to fields. Store only a salted hash, and review hashing parameters as hardware changes. If you check passwords against a breached-password service, document the data flow, contractual arrangements and cross-border handling required for Australian privacy compliance rather than sending the secret to an unexamined endpoint.
Roll the change out in stages across an Australian product rather than switching every form at once. A distributed service team may support different release windows, and weekend traffic can differ sharply from weekday traffic. Watch anonymised failure rates, support contacts and account-abuse signals by release cohort, while confirming that no raw password appears in client or server telemetry.
Create a feature branch today: add the validator and its tests, connect it to one real sign-up form, verify that the API applies the same policy, and release the first measured version with accessible feedback.