Skip to content

Commit 3af8f3f

Browse files
committed
Added Documentation
1 parent f7a03b8 commit 3af8f3f

File tree

2 files changed

+214
-0
lines changed

2 files changed

+214
-0
lines changed

docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
***** xref:servlet/authentication/passwords/password-encoder.adoc[PasswordEncoder]
4949
***** xref:servlet/authentication/passwords/dao-authentication-provider.adoc[DaoAuthenticationProvider]
5050
***** xref:servlet/authentication/passwords/ldap.adoc[LDAP]
51+
***** xref:servlet/authentication/passwords/management.adoc[Password Management]
5152
*** xref:servlet/authentication/persistence.adoc[Persistence]
5253
*** xref:servlet/authentication/passkeys.adoc[Passkeys]
5354
*** xref:servlet/authentication/onetimetoken.adoc[One-Time Token]
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
= Password Management
2+
3+
Spring Security can offer advice about passwords through its `PasswordAdvisor` API.
4+
It aims to help applications apply https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/docs_en/OWASP_Application_Security_Verification_Standard_5.0.0_en.csv#L108[the ASVS 5.0 standard for Password Security].
5+
6+
Consider the following configuration:
7+
8+
.Java
9+
[source,java,role="primary"]
10+
----
11+
http
12+
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
13+
.formLogin(Customizer.withDefaults())
14+
.passwordManagement(Customizer.withDefaults())
15+
----
16+
17+
By adding the `passwordManagement` DSL, your application now has the ability to suggest or require a user to change their password if certain criteria are met.
18+
19+
By default, `UserDetailsPasswordAdvisor` is consulted in a `SessionAuthenticationStrategy` after login is complete.
20+
It calls `UserDetails#getPasswordAdvice` to look up any password advice stored on the user object.
21+
If the advice on the user object is `PasswordAdvice.MUST_CHANGE`, then Spring Security will redirect the application to `/change-password` for every request in the application.
22+
23+
== Configuring a Password Advisor
24+
25+
There are wo kinds of advisors, `PasswordAdvisor` and `UpdatePasswordAdvisor`.
26+
The first advisor type is for analyzing the password at authentication time.
27+
This is useful should your website's password standards change or should the password become compromised and leaked.
28+
It is also useful when your administrator has marked a certain user as needing to update their password, regardless of any other analysis.
29+
30+
To take advantage of these advisors, you can publish a `PasswordAdvisor` as a bean:
31+
32+
[tabs]
33+
======
34+
Java::
35+
+
36+
[source,java,role="primary"]
37+
----
38+
@Bean
39+
PasswordAdvisor passwordAdvisor() {
40+
return CompositePasswordAdvisor.withDefaults( // <1>
41+
new CompromisedPasswordAdvisor() // <2>
42+
);
43+
}
44+
----
45+
46+
Kotlin::
47+
+
48+
[source,kotlin,role="secondary"]
49+
----
50+
@Bean
51+
fun passwordAdvisor(): PasswordAdvisor {
52+
return CompositePasswordAdvisor.withDefaults( // <1>
53+
CompromisedPasswordAdvisor() // <2>
54+
)
55+
}
56+
----
57+
======
58+
<1> - `withDefaults` adds an advisor that checks the `UserDetails` object for any admin-set password action and an advisor that checks password length
59+
<2> - An advisor that checks the HaveIBeenPwned breached password database
60+
61+
== Requiring Password Changes
62+
63+
By default password advisors mark a password as `SHOULD_CHANGE`.
64+
This allows you to add this to your application passively.
65+
66+
In the event you want to start requiring that users change their password, you can configure each password advisor with a policy.
67+
For example, you can state that whenever a password is compromised, force the user to update their password at that time by configuring the `CompromisedPasswordAdvisor` policy as `MUST_CHANGE`:
68+
69+
[tabs]
70+
======
71+
Java::
72+
+
73+
[source,java,role="primary"]
74+
----
75+
@Bean
76+
PasswordAdvisor passwordAdvisor() {
77+
CompromisedPasswordAdvisor compromised = new CompromisedPasswordAdvisor();
78+
compromised.setAction(PasswordAction.MUST_CHANGE);
79+
return CompositePasswordAdvisor.withDefaults(compromised);
80+
}
81+
----
82+
83+
Kotlin::
84+
+
85+
[source,kotlin,role="secondary"]
86+
----
87+
@Bean
88+
fun passwordAdvisor(): PasswordAdvisor {
89+
val compromised = CompromisedPasswordAdvisor()
90+
compromised.setAction(PasswordAction.MUST_CHANGE)
91+
return CompositePasswordAdvisor.withDefaults(compromise)
92+
}
93+
----
94+
======
95+
96+
[TIP]
97+
====
98+
While optional, it's helpful to include `UserDetailsPasswordAdvisor` in the set of password advisors as this allows admins to update the `passwordAction` value in `UserDetails` out-of-band and thus require password changes en masse.
99+
This is included by default when calling `withDefaults`.
100+
====
101+
102+
== Updating Passwords
103+
104+
When a user updates their password, the following are recommended:
105+
106+
1. You require the user provide their old password
107+
2. You require the user provide and confirm their new password
108+
3. You test the password against a set of `UpdatePasswordAdvisor` instances
109+
4. You update both the password and any remaining password action
110+
5. You log out the individual so they can re-login with their new password
111+
112+
Here is a sample controller that does this:
113+
114+
[tabs]
115+
======
116+
Java::
117+
+
118+
[source,java,role="primary"]
119+
----
120+
@Controller
121+
class ChangePasswordController {
122+
private final InMemoryUserDetailsManager users;
123+
124+
private final PasswordEncoder passwordEncoder =
125+
PasswordEncoderFactories.createDelegatingPasswordEncoder();
126+
127+
private final UpdatePasswordAdvisor passwordAdvisor = CompositeUpdatePasswordAdvisor.withDefaults(
128+
new CompromisedPasswordAdvisor(),
129+
new LengthPasswordAdvisor(12) // <1>
130+
);
131+
132+
// constructor
133+
134+
@PostMapping("/change-password")
135+
String changePassword(Passwords passwords, @AuthenticationPrincipal UserDetails user,
136+
HttpServletRequest request, HttpServletResponse response) {
137+
138+
UserDetails latest = this.users.findUserByUsername(user.getUsername());
139+
if (!this.passwordEncoder.matches(latest.getPassword(), passwords.current())) { // <2>
140+
request.setAttribute("error", "The provided current password doesn't match your password on file.");
141+
return "change-password";
142+
}
143+
if (!passwords.change().equals(passwords.confirm())) { // <3>
144+
request.setAttribute("error", "The new password doesn't match its confirmation.");
145+
return "change-password";
146+
}
147+
PasswordAdvice advice = this.passwordAdvisor.advise(latest, latest.getPassword(), passwords.change()); // <4>
148+
if (PasswordAction.NONE.advisedBy(advice)) {
149+
UserDetails updated = User.withUserDetails(latest)
150+
.passwordEncoder(this.passwordEncoder::encode)
151+
.password(passwords.change())
152+
.passwordAction(PasswordAction.NONE).build(); // <5>
153+
this.users.updateUser(updated);
154+
return "forward:/logout"; // <6>
155+
}
156+
request.setAttribute(error, "Your password was rejected since " + advice);
157+
return "change-password";
158+
}
159+
}
160+
----
161+
162+
Kotlin::
163+
+
164+
[source,kotlin,role="secondary"]
165+
----
166+
@Controller
167+
open class ChangePasswordController {
168+
private val users: InMemoryUserDetailsManager
169+
170+
private val passwordEncoder =
171+
PasswordEncoderFactories.createDelegatingPasswordEncoder()
172+
173+
private val passwordAdvisor = CompositeUpdatePasswordAdvisor.of(
174+
CompromisedPasswordAdvisor(),
175+
LengthPasswordAdvisor(12) // <1>
176+
)
177+
178+
// constructor
179+
180+
@PostMapping("/change-password")
181+
fun changePassword(val passwords: Passwords, @AuthenticationPrincipal val user: UserDetails,
182+
val request: HttpServletRequest): String {
183+
184+
val latest = this.users.findUserByUsername(user.getUsername())
185+
if (!this.passwordEncoder.matches(latest.getPassword(), passwords.current())) { // <2>
186+
request.setAttribute("error", "The provided current password doesn't match your password on file.")
187+
return "change-password"
188+
}
189+
if (!passwords.change().equals(passwords.confirm())) { // <3>
190+
request.setAttribute("error", "The new password doesn't match its confirmation.")
191+
return "change-password"
192+
}
193+
val advice = this.passwordAdvisor.advise(latest, latest.getPassword(), passwords.change()) // <4>
194+
if (PasswordAction.NONE.advisedBy(advice)) {
195+
val updated = User.withUserDetails(latest)
196+
.passwordEncoder(this.passwordEncoder::encode)
197+
.password(passwords.change())
198+
.passwordAction(PasswordAction.NONE).build() // <5>
199+
this.users.updateUser(updated)
200+
return "forward:/logout" // <6>
201+
}
202+
request.setAttribute(error, "Your password was rejected since " + advice)
203+
return "change-password"
204+
}
205+
}
206+
----
207+
======
208+
<1> - Override the default `PasswordLengthAdvisor` to require a minimum length of 12
209+
<2> - Test that the user's current password matches the provided password; note that since credentials are often erased during login, you'll need to look up the user in order to check their password
210+
<3> - Test that the new password and the confirmation fields match
211+
<4> - Test the password against various criteria
212+
<5> - If all the is met, then update the `UserDetails` object to have the new password and no more password advice
213+
<6> - Forward to /logout to get the person logged out

0 commit comments

Comments
 (0)