onetimetoken.adoc 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. [[one-time-token-login]]
  2. = One-Time Token Login
  3. Spring Security offers support for One-Time Token (OTT) authentication via the `oneTimeTokenLogin()` DSL.
  4. Before diving into implementation details, it's important to clarify the scope of the OTT feature within the framework, highlighting what is supported and what isn't.
  5. == Understanding One-Time Tokens vs. One-Time Passwords
  6. It's common to confuse One-Time Tokens (OTT) with https://en.wikipedia.org/wiki/One-time_password[One-Time Passwords] (OTP), but in Spring Security, these concepts differ in several key ways.
  7. For clarity, we'll assume OTP refers to https://en.wikipedia.org/wiki/Time-based_one-time_password[TOTP] (Time-Based One-Time Password) or https://en.wikipedia.org/wiki/HMAC-based_one-time_password[HOTP] (HMAC-Based One-Time Password).
  8. === Setup Requirements
  9. - OTT: No initial setup is required. The user doesn't need to configure anything in advance.
  10. - OTP: Typically requires setup, such as generating and sharing a secret key with an external tool to produce the one-time passwords.
  11. === Token Delivery
  12. - OTT: Usually a custom javadoc:org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler[] must be implemented, responsible for delivering the token to the end user.
  13. - OTP: The token is often generated by an external tool, so there's no need to send it to the user via the application.
  14. === Token Generation
  15. - OTT: The javadoc:org.springframework.security.authentication.ott.OneTimeTokenService#generate(org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest)[] method requires a javadoc:org.springframework.security.authentication.ott.OneTimeToken[] to be returned, emphasizing server-side generation.
  16. - OTP: The token is not necessarily generated on the server side, it's often created by the client using the shared secret.
  17. In summary, One-Time Tokens (OTT) provide a way to authenticate users without additional account setup, differentiating them from One-Time Passwords (OTP), which typically involve a more complex setup process and rely on external tools for token generation.
  18. The One-Time Token Login works in two major steps.
  19. 1. User requests a token by submitting their user identifier, usually the username, and the token is delivered to them, often as a Magic Link, via e-mail, SMS, etc.
  20. 2. User submits the token to the one-time token login endpoint and, if valid, the user gets logged in.
  21. In the following sections we will explore how to configure OTT Login for your needs.
  22. - <<default-pages,Understanding the integration with the default generated login page>>
  23. - <<sending-token-to-user,Sending the token to the user>>
  24. - <<changing-submit-page-url,Configuring the One-Time Token submit page>>
  25. - <<changing-generate-url,Changing the One-Time Token generate URL>>
  26. - <<customize-generate-consume-token,Customize how to generate and consume tokens>>
  27. [[default-pages]]
  28. == Default Login Page and Default One-Time Token Submit Page
  29. The `oneTimeTokenLogin()` DSL can be used in conjunction with `formLogin()`, which will produce an additional One-Time Token Request Form in the xref:servlet/authentication/passwords/form.adoc[default generated login page].
  30. It will also set up the javadoc:org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter[] to generate a default One-Time Token submit page.
  31. [[sending-token-to-user]]
  32. == Sending the Token to the User
  33. It is not possible for Spring Security to reasonably determine the way the token should be delivered to your users.
  34. Therefore, a custom javadoc:org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler[] must be provided to deliver the token to the user based on your needs.
  35. One of the most common delivery strategies is a Magic Link, via e-mail, SMS, etc.
  36. In the following example, we are going to create a magic link and sent it to the user's email.
  37. .One-Time Token Login Configuration
  38. [tabs]
  39. ======
  40. Java::
  41. +
  42. [source,java,role="primary"]
  43. ----
  44. @Configuration
  45. @EnableWebSecurity
  46. public class SecurityConfig {
  47. @Bean
  48. public SecurityFilterChain filterChain(HttpSecurity http) {
  49. http
  50. // ...
  51. .formLogin(Customizer.withDefaults())
  52. .oneTimeTokenLogin(Customizer.withDefaults());
  53. return http.build();
  54. }
  55. }
  56. import org.springframework.mail.SimpleMailMessage;
  57. import org.springframework.mail.javamail.JavaMailSender;
  58. @Component <1>
  59. public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
  60. private final MailSender mailSender;
  61. private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
  62. // constructor omitted
  63. @Override
  64. public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
  65. UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
  66. .replacePath(request.getContextPath())
  67. .replaceQuery(null)
  68. .fragment(null)
  69. .path("/login/ott")
  70. .queryParam("token", oneTimeToken.getTokenValue()); <2>
  71. String magicLink = builder.toUriString();
  72. String email = getUserEmail(oneTimeToken.getUsername()); <3>
  73. this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: " + magicLink); <4>
  74. this.redirectHandler.handle(request, response, oneTimeToken); <5>
  75. }
  76. private String getUserEmail() {
  77. // ...
  78. }
  79. }
  80. @Controller
  81. class PageController {
  82. @GetMapping("/ott/sent")
  83. String ottSent() {
  84. return "my-template";
  85. }
  86. }
  87. ----
  88. Kotlin::
  89. +
  90. [source,kotlin,role="secondary"]
  91. ----
  92. @Configuration
  93. @EnableWebSecurity
  94. class SecurityConfig {
  95. @Bean
  96. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  97. http{
  98. formLogin {}
  99. oneTimeTokenLogin { }
  100. }
  101. return http.build()
  102. }
  103. }
  104. import org.springframework.mail.SimpleMailMessage;
  105. import org.springframework.mail.javamail.JavaMailSender;
  106. @Component (1)
  107. class MagicLinkOneTimeTokenGenerationSuccessHandler(
  108. private val mailSender: MailSender,
  109. private val redirectHandler: OneTimeTokenGenerationSuccessHandler = RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
  110. ) : OneTimeTokenGenerationSuccessHandler {
  111. override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
  112. val builder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
  113. .replacePath(request.contextPath)
  114. .replaceQuery(null)
  115. .fragment(null)
  116. .path("/login/ott")
  117. .queryParam("token", oneTimeToken.getTokenValue()) (2)
  118. val magicLink = builder.toUriString()
  119. val email = getUserEmail(oneTimeToken.getUsername()) (3)
  120. this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: $magicLink")(4)
  121. this.redirectHandler.handle(request, response, oneTimeToken) (5)
  122. }
  123. private fun getUserEmail(): String {
  124. // ...
  125. }
  126. }
  127. @Controller
  128. class PageController {
  129. @GetMapping("/ott/sent")
  130. fun ottSent(): String {
  131. return "my-template"
  132. }
  133. }
  134. ----
  135. ======
  136. <1> Make the `MagicLinkOneTimeTokenGenerationSuccessHandler` a Spring bean
  137. <2> Create a login processing URL with the `token` as a query param
  138. <3> Retrieve the user's email based on the username
  139. <4> Use the `JavaMailSender` API to send the email to the user with the magic link
  140. <5> Use the `RedirectOneTimeTokenGenerationSuccessHandler` to perform a redirect to your desired URL
  141. The email content will look similar to:
  142. > Use the following link to sign in into the application: \http://localhost:8080/login/ott?token=a830c444-29d8-4d98-9b46-6aba7b22fe5b
  143. The default submit page will detect that the URL has the `token` query param and will automatically fill the form field with the token value.
  144. [[changing-generate-url]]
  145. == Changing the One-Time Token Generate URL
  146. By default, the javadoc:org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter[] listens to `POST /ott/generate` requests.
  147. That URL can be changed by using the `generateTokenUrl(String)` DSL method:
  148. .Changing the Generate URL
  149. [tabs]
  150. ======
  151. Java::
  152. +
  153. [source,java,role="primary"]
  154. ----
  155. @Configuration
  156. @EnableWebSecurity
  157. public class SecurityConfig {
  158. @Bean
  159. public SecurityFilterChain filterChain(HttpSecurity http) {
  160. http
  161. // ...
  162. .formLogin(Customizer.withDefaults())
  163. .oneTimeTokenLogin((ott) -> ott
  164. .generateTokenUrl("/ott/my-generate-url")
  165. );
  166. return http.build();
  167. }
  168. }
  169. @Component
  170. public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
  171. // ...
  172. }
  173. ----
  174. Kotlin::
  175. +
  176. [source,kotlin,role="secondary"]
  177. ----
  178. @Configuration
  179. @EnableWebSecurity
  180. class SecurityConfig {
  181. @Bean
  182. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  183. http {
  184. //...
  185. formLogin { }
  186. oneTimeTokenLogin {
  187. generateTokenUrl = "/ott/my-generate-url"
  188. }
  189. }
  190. return http.build()
  191. }
  192. }
  193. @Component
  194. class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
  195. // ...
  196. }
  197. ----
  198. ======
  199. [[changing-submit-page-url]]
  200. == Changing the Default Submit Page URL
  201. The default One-Time Token submit page is generated by the javadoc:org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter[] and listens to `GET /login/ott`.
  202. The URL can also be changed, like so:
  203. .Configuring the Default Submit Page URL
  204. [tabs]
  205. ======
  206. Java::
  207. +
  208. [source,java,role="primary"]
  209. ----
  210. @Configuration
  211. @EnableWebSecurity
  212. public class SecurityConfig {
  213. @Bean
  214. public SecurityFilterChain filterChain(HttpSecurity http) {
  215. http
  216. // ...
  217. .formLogin(Customizer.withDefaults())
  218. .oneTimeTokenLogin((ott) -> ott
  219. .submitPageUrl("/ott/submit")
  220. );
  221. return http.build();
  222. }
  223. }
  224. @Component
  225. public class MagicLinkGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
  226. // ...
  227. }
  228. ----
  229. Kotlin::
  230. +
  231. [source,kotlin,role="secondary"]
  232. ----
  233. @Configuration
  234. @EnableWebSecurity
  235. class SecurityConfig {
  236. @Bean
  237. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  238. http {
  239. //...
  240. formLogin { }
  241. oneTimeTokenLogin {
  242. submitPageUrl = "/ott/submit"
  243. }
  244. }
  245. return http.build()
  246. }
  247. }
  248. @Component
  249. class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
  250. // ...
  251. }
  252. ----
  253. ======
  254. [[disabling-default-submit-page]]
  255. == Disabling the Default Submit Page
  256. If you want to use your own One-Time Token submit page, you can disable the default page and then provide your own endpoint.
  257. .Disabling the Default Submit Page
  258. [tabs]
  259. ======
  260. Java::
  261. +
  262. [source,java,role="primary"]
  263. ----
  264. @Configuration
  265. @EnableWebSecurity
  266. public class SecurityConfig {
  267. @Bean
  268. public SecurityFilterChain filterChain(HttpSecurity http) {
  269. http
  270. .authorizeHttpRequests((authorize) -> authorize
  271. .requestMatchers("/my-ott-submit").permitAll()
  272. .anyRequest().authenticated()
  273. )
  274. .formLogin(Customizer.withDefaults())
  275. .oneTimeTokenLogin((ott) -> ott
  276. .showDefaultSubmitPage(false)
  277. );
  278. return http.build();
  279. }
  280. }
  281. @Controller
  282. public class MyController {
  283. @GetMapping("/my-ott-submit")
  284. public String ottSubmitPage() {
  285. return "my-ott-submit";
  286. }
  287. }
  288. @Component
  289. public class OneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
  290. // ...
  291. }
  292. ----
  293. Kotlin::
  294. +
  295. [source,kotlin,role="secondary"]
  296. ----
  297. @Configuration
  298. @EnableWebSecurity
  299. class SecurityConfig {
  300. @Bean
  301. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  302. http {
  303. authorizeHttpRequests {
  304. authorize("/my-ott-submit", authenticated)
  305. authorize(anyRequest, authenticated)
  306. }
  307. formLogin { }
  308. oneTimeTokenLogin {
  309. showDefaultSubmitPage = false
  310. }
  311. }
  312. return http.build()
  313. }
  314. }
  315. @Controller
  316. class MyController {
  317. @GetMapping("/my-ott-submit")
  318. fun ottSubmitPage(): String {
  319. return "my-ott-submit"
  320. }
  321. }
  322. @Component
  323. class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
  324. // ...
  325. }
  326. ----
  327. ======
  328. [[customize-generate-consume-token]]
  329. == Customize How to Generate and Consume One-Time Tokens
  330. The interface that define the common operations for generating and consuming one-time tokens is the javadoc:org.springframework.security.authentication.ott.OneTimeTokenService[].
  331. Spring Security uses the javadoc:org.springframework.security.authentication.ott.InMemoryOneTimeTokenService[] as the default implementation of that interface, if none is provided.
  332. For production environments consider using javadoc:org.springframework.security.authentication.ott.JdbcOneTimeTokenService[].
  333. Some of the most common reasons to customize the `OneTimeTokenService` are, but not limited to:
  334. - Changing the one-time token expire time
  335. - Storing more information from the generate token request
  336. - Changing how the token value is created
  337. - Additional validation when consuming a one-time token
  338. There are two options to customize the `OneTimeTokenService`.
  339. One option is to provide it as a bean, so it can be automatically be picked-up by the `oneTimeTokenLogin()` DSL:
  340. .Passing the OneTimeTokenService as a Bean
  341. [tabs]
  342. ======
  343. Java::
  344. +
  345. [source,java,role="primary"]
  346. ----
  347. @Configuration
  348. @EnableWebSecurity
  349. public class SecurityConfig {
  350. @Bean
  351. public SecurityFilterChain filterChain(HttpSecurity http) {
  352. http
  353. // ...
  354. .formLogin(Customizer.withDefaults())
  355. .oneTimeTokenLogin(Customizer.withDefaults());
  356. return http.build();
  357. }
  358. @Bean
  359. public OneTimeTokenService oneTimeTokenService() {
  360. return new MyCustomOneTimeTokenService();
  361. }
  362. }
  363. @Component
  364. public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
  365. // ...
  366. }
  367. ----
  368. Kotlin::
  369. +
  370. [source,kotlin,role="secondary"]
  371. ----
  372. @Configuration
  373. @EnableWebSecurity
  374. class SecurityConfig {
  375. @Bean
  376. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  377. http {
  378. //...
  379. formLogin { }
  380. oneTimeTokenLogin { }
  381. }
  382. return http.build()
  383. }
  384. @Bean
  385. open fun oneTimeTokenService(): OneTimeTokenService {
  386. return MyCustomOneTimeTokenService()
  387. }
  388. }
  389. @Component
  390. class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
  391. // ...
  392. }
  393. ----
  394. ======
  395. The second option is to pass the `OneTimeTokenService` instance to the DSL, which is useful if there are multiple `SecurityFilterChain` and a different `OneTimeTokenService` is needed for each of them.
  396. .Passing the OneTimeTokenService using the DSL
  397. [tabs]
  398. ======
  399. Java::
  400. +
  401. [source,java,role="primary"]
  402. ----
  403. @Configuration
  404. @EnableWebSecurity
  405. public class SecurityConfig {
  406. @Bean
  407. public SecurityFilterChain filterChain(HttpSecurity http) {
  408. http
  409. // ...
  410. .formLogin(Customizer.withDefaults())
  411. .oneTimeTokenLogin((ott) -> ott
  412. .oneTimeTokenService(new MyCustomOneTimeTokenService())
  413. );
  414. return http.build();
  415. }
  416. }
  417. @Component
  418. public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
  419. // ...
  420. }
  421. ----
  422. Kotlin::
  423. +
  424. [source,kotlin,role="secondary"]
  425. ----
  426. @Configuration
  427. @EnableWebSecurity
  428. class SecurityConfig {
  429. @Bean
  430. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  431. http {
  432. //...
  433. formLogin { }
  434. oneTimeTokenLogin {
  435. oneTimeTokenService = MyCustomOneTimeTokenService()
  436. }
  437. }
  438. return http.build()
  439. }
  440. }
  441. @Component
  442. class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
  443. // ...
  444. }
  445. ----
  446. ======
  447. [[customize-generate-token-request]]
  448. == Customize GenerateOneTimeTokenRequest Instance
  449. There are a number of reasons that you may want to adjust an GenerateOneTimeTokenRequest. For example, you may want expiresIn to be set to 10 mins, which Spring Security sets to 5 mins by default.
  450. You can customize elements of GenerateOneTimeTokenRequest by publishing an GenerateOneTimeTokenRequestResolver as a @Bean, like so:
  451. [tabs]
  452. ======
  453. Java::
  454. +
  455. [source,java,role="primary"]
  456. ----
  457. @Bean
  458. GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
  459. DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver();
  460. return (request) -> {
  461. GenerateOneTimeTokenRequest generate = delegate.resolve(request);
  462. return new GenerateOneTimeTokenRequest(generate.getUsername(), Duration.ofSeconds(600));
  463. };
  464. }
  465. ----
  466. Kotlin::
  467. +
  468. [source,kotlin,role="secondary"]
  469. ----
  470. @Bean
  471. fun generateRequestResolver() : GenerateOneTimeTokenRequestResolver {
  472. return DefaultGenerateOneTimeTokenRequestResolver().apply {
  473. this.setExpiresIn(Duration.ofMinutes(10))
  474. }
  475. }
  476. ----
  477. ======