onetimetoken.adoc 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  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.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler[] 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.reactive.ReactiveOneTimeTokenService#generate(org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest)[] method requires a javadoc:org.springframework.security.authentication.ott.OneTimeToken[], wrapped in Mono, 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.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter[] 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.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler[] 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. @EnableWebFluxSecurity
  46. public class SecurityConfig {
  47. @Bean
  48. public SecurityWebFilterChain filterChain(ServerHttpSecurity 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 ServerOneTimeTokenGenerationSuccessHandler {
  60. private final MailSender mailSender;
  61. private final ServerOneTimeTokenGenerationSuccessHandler redirectHandler = new ServerRedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
  62. // constructor omitted
  63. @Override
  64. public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
  65. return Mono.just(exchange.getRequest())
  66. .map((request) ->
  67. UriComponentsBuilder.fromUri(request.getURI())
  68. .replacePath(request.getPath().contextPath().value())
  69. .replaceQuery(null)
  70. .fragment(null)
  71. .path("/login/ott")
  72. .queryParam("token", oneTimeToken.getTokenValue())
  73. .toUriString() <2>
  74. )
  75. .flatMap((uri) -> this.mailSender.send(getUserEmail(oneTimeToken.getUsername()), <3>
  76. "Use the following link to sign in into the application: " + magicLink)) <4>
  77. .then(this.redirectHandler.handle(exchange, oneTimeToken)); <5>
  78. }
  79. private String getUserEmail() {
  80. // ...
  81. }
  82. }
  83. @Controller
  84. class PageController {
  85. @GetMapping("/ott/sent")
  86. String ottSent() {
  87. return "my-template";
  88. }
  89. }
  90. ----
  91. Kotlin::
  92. +
  93. [source,kotlin,role="secondary"]
  94. ----
  95. @Configuration
  96. @EnableWebFluxSecurity
  97. class SecurityConfig {
  98. open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
  99. return http {
  100. authorizeExchange {
  101. authorize(anyExchange, authenticated)
  102. }
  103. oneTimeTokenLogin { }
  104. }
  105. }
  106. }
  107. @Component (1)
  108. class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
  109. private val redirectStrategy: ServerRedirectStrategy = DefaultServerRedirectStrategy()
  110. override fun handle(exchange: ServerWebExchange, oneTimeToken: OneTimeToken): Mono<Void> {
  111. val builder = UriComponentsBuilder.fromUri(exchange.request.uri)
  112. .replacePath(null)
  113. .replaceQuery(null)
  114. .fragment(null)
  115. .path("/login/ott")
  116. .queryParam("token", oneTimeToken.getTokenValue()) (2)
  117. val magicLink = builder.toUriString()
  118. builder.replacePath(null)
  119. .replaceQuery(null)
  120. .path("/ott/sent")
  121. val redirectLink = builder.toUriString()
  122. return this.mailSender.send(
  123. getUserEmail(oneTimeToken.getUsername()), (3)
  124. "Use the following link to sign in into the application: $magicLink") (4)
  125. .then(this.redirectStrategy.sendRedirect(exchange, URI.create(redirectLink))) (5)
  126. }
  127. private String getUserEmail() {
  128. // ...
  129. }
  130. }
  131. @Controller
  132. class PageController {
  133. @GetMapping("/ott/sent")
  134. fun ottSent(): String {
  135. return "my-template"
  136. }
  137. }
  138. ----
  139. ======
  140. <1> Make the `MagicLinkOneTimeTokenGenerationSuccessHandler` a Spring bean
  141. <2> Create a login processing URL with the `token` as a query param
  142. <3> Retrieve the user's email based on the username
  143. <4> Use the `MailSender` API to send the email to the user with the magic link
  144. <5> Use the `ServerRedirectStrategy` to perform a redirect to your desired URL
  145. The email content will look similar to:
  146. > Use the following link to sign in into the application: \http://localhost:8080/login/ott?token=a830c444-29d8-4d98-9b46-6aba7b22fe5b
  147. 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.
  148. [[changing-generate-url]]
  149. == Changing the One-Time Token Generate URL
  150. By default, the javadoc:org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter[] listens to `POST /ott/generate` requests.
  151. That URL can be changed by using the `generateTokenUrl(String)` DSL method:
  152. .Changing the Generate URL
  153. [tabs]
  154. ======
  155. Java::
  156. +
  157. [source,java,role="primary"]
  158. ----
  159. @Configuration
  160. @EnableWebFluxSecurity
  161. public class SecurityConfig {
  162. @Bean
  163. public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
  164. http
  165. // ...
  166. .formLogin(Customizer.withDefaults())
  167. .oneTimeTokenLogin((ott) -> ott
  168. .generateTokenUrl("/ott/my-generate-url")
  169. );
  170. return http.build();
  171. }
  172. }
  173. @Component
  174. public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
  175. // ...
  176. }
  177. ----
  178. Kotlin::
  179. +
  180. [source,kotlin,role="secondary"]
  181. ----
  182. @Configuration
  183. @EnableWebFluxSecurity
  184. class SecurityConfig {
  185. open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
  186. return http {
  187. // ...
  188. formLogin { }
  189. oneTimeTokenLogin {
  190. generateTokenUrl = "/ott/my-generate-url"
  191. }
  192. }
  193. }
  194. }
  195. @Component
  196. class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
  197. // ...
  198. }
  199. ----
  200. ======
  201. [[changing-submit-page-url]]
  202. == Changing the Default Submit Page URL
  203. The default One-Time Token submit page is generated by the javadoc:org.springframework.security.web.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter[] and listens to `GET /login/ott`.
  204. The URL can also be changed, like so:
  205. .Configuring the Default Submit Page URL
  206. [tabs]
  207. ======
  208. Java::
  209. +
  210. [source,java,role="primary"]
  211. ----
  212. @Configuration
  213. @EnableWebFluxSecurity
  214. public class SecurityConfig {
  215. @Bean
  216. public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
  217. http
  218. // ...
  219. .formLogin(Customizer.withDefaults())
  220. .oneTimeTokenLogin((ott) -> ott
  221. .submitPageUrl("/ott/submit")
  222. );
  223. return http.build();
  224. }
  225. }
  226. @Component
  227. public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
  228. // ...
  229. }
  230. ----
  231. Kotlin::
  232. +
  233. [source,kotlin,role="secondary"]
  234. ----
  235. @Configuration
  236. @EnableWebFluxSecurity
  237. class SecurityConfig {
  238. open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
  239. return http {
  240. // ...
  241. formLogin { }
  242. oneTimeTokenLogin {
  243. submitPageUrl = "/ott/submit"
  244. }
  245. }
  246. }
  247. }
  248. @Component
  249. class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
  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. @EnableWebFluxSecurity
  266. public class SecurityConfig {
  267. @Bean
  268. public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
  269. http
  270. .authorizeExchange((authorize) -> authorize
  271. .pathMatchers("/my-ott-submit").permitAll()
  272. .anyExchange().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 MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
  290. // ...
  291. }
  292. ----
  293. Kotlin::
  294. +
  295. [source,kotlin,role="secondary"]
  296. ----
  297. @Configuration
  298. @EnableWebFluxSecurity
  299. class SecurityConfig {
  300. open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
  301. return http {
  302. authorizeExchange {
  303. authorize(pathMatchers("/my-ott-submit"), permitAll)
  304. authorize(anyExchange, authenticated)
  305. }
  306. .formLogin { }
  307. oneTimeTokenLogin {
  308. showDefaultSubmitPage = false
  309. }
  310. }
  311. }
  312. }
  313. @Controller
  314. class MyController {
  315. @GetMapping("/my-ott-submit")
  316. fun ottSubmitPage(): String {
  317. return "my-ott-submit"
  318. }
  319. }
  320. @Component
  321. class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
  322. // ...
  323. }
  324. ----
  325. ======
  326. [[customize-generate-consume-token]]
  327. == Customize How to Generate and Consume One-Time Tokens
  328. The interface that define the common operations for generating and consuming one-time tokens is the javadoc:org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService[].
  329. Spring Security uses the javadoc:org.springframework.security.authentication.ott.reactive.InMemoryReactiveOneTimeTokenService[] as the default implementation of that interface, if none is provided.
  330. Some of the most common reasons to customize the `ReactiveOneTimeTokenService` are, but not limited to:
  331. - Changing the one-time token expire time
  332. - Storing more information from the generate token request
  333. - Changing how the token value is created
  334. - Additional validation when consuming a one-time token
  335. There are two options to customize the `ReactiveOneTimeTokenService`.
  336. One option is to provide it as a bean, so it can be automatically be picked-up by the `oneTimeTokenLogin()` DSL:
  337. .Passing the ReactiveOneTimeTokenService as a Bean
  338. [tabs]
  339. ======
  340. Java::
  341. +
  342. [source,java,role="primary"]
  343. ----
  344. @Configuration
  345. @EnableWebFluxSecurity
  346. public class SecurityConfig {
  347. @Bean
  348. public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
  349. http
  350. // ...
  351. .formLogin(Customizer.withDefaults())
  352. .oneTimeTokenLogin(Customizer.withDefaults());
  353. return http.build();
  354. }
  355. @Bean
  356. public ReactiveOneTimeTokenService oneTimeTokenService() {
  357. return new MyCustomReactiveOneTimeTokenService();
  358. }
  359. }
  360. @Component
  361. public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
  362. // ...
  363. }
  364. ----
  365. Kotlin::
  366. +
  367. [source,kotlin,role="secondary"]
  368. ----
  369. @Configuration
  370. @EnableWebFluxSecurity
  371. class SecurityConfig {
  372. open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
  373. return http {
  374. //..
  375. .formLogin { }
  376. oneTimeTokenLogin { }
  377. }
  378. }
  379. @Bean
  380. open fun oneTimeTokenService():ReactiveOneTimeTokenService {
  381. return MyCustomReactiveOneTimeTokenService();
  382. }
  383. }
  384. @Component
  385. class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
  386. // ...
  387. }
  388. ----
  389. ======
  390. The second option is to pass the `ReactiveOneTimeTokenService` instance to the DSL, which is useful if there are multiple ``SecurityWebFilterChain``s and a different ``ReactiveOneTimeTokenService``s is needed for each of them.
  391. .Passing the ReactiveOneTimeTokenService using the DSL
  392. [tabs]
  393. ======
  394. Java::
  395. +
  396. [source,java,role="primary"]
  397. ----
  398. @Configuration
  399. @EnableWebFluxSecurity
  400. public class SecurityConfig {
  401. @Bean
  402. public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
  403. http
  404. // ...
  405. .formLogin(Customizer.withDefaults())
  406. .oneTimeTokenLogin((ott) -> ott
  407. .oneTimeTokenService(new MyCustomReactiveOneTimeTokenService())
  408. );
  409. return http.build();
  410. }
  411. }
  412. @Component
  413. public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
  414. // ...
  415. }
  416. ----
  417. Kotlin::
  418. +
  419. [source,kotlin,role="secondary"]
  420. ----
  421. @Configuration
  422. @EnableWebFluxSecurity
  423. class SecurityConfig {
  424. open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
  425. return http {
  426. //..
  427. .formLogin { }
  428. oneTimeTokenLogin {
  429. oneTimeTokenService = MyCustomReactiveOneTimeTokenService()
  430. }
  431. }
  432. }
  433. }
  434. @Component
  435. class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
  436. // ...
  437. }
  438. ----
  439. ======