onetimetoken.adoc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  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.GeneratedOneTimeTokenHandler[] 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. [[default-pages]]
  22. == Default Login Page and Default One-Time Token Submit Page
  23. 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].
  24. It will also set up the javadoc:org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter[] to generate a default One-Time Token submit page.
  25. In the following sections we will explore how to configure OTT Login for your needs.
  26. - <<sending-token-to-user,Sending the token to the user>>
  27. - <<changing-submit-page-url,Configuring the One-Time Token submit page>>
  28. - <<changing-generate-url,Changing the One-Time Token generate URL>>
  29. [[sending-token-to-user]]
  30. == Sending the Token to the User
  31. It is not possible for Spring Security to reasonably determine the way the token should be delivered to your users.
  32. Therefore, a custom javadoc:org.springframework.security.web.authentication.ott.GeneratedOneTimeTokenHandler[] must be provided to deliver the token to the user based on your needs.
  33. One of the most common delivery strategies is a Magic Link, via e-mail, SMS, etc.
  34. In the following example, we are going to create a magic link and sent it to the user's email.
  35. .One-Time Token Login Configuration
  36. [tabs]
  37. ======
  38. Java::
  39. +
  40. [source,java,role="primary"]
  41. ----
  42. @Configuration
  43. @EnableWebSecurity
  44. public class SecurityConfig {
  45. @Bean
  46. public SecurityFilterChain filterChain(HttpSecurity http, MagicLinkGeneratedOneTimeTokenSuccessHandler magicLinkSender) {
  47. http
  48. // ...
  49. .formLogin(Customizer.withDefaults())
  50. .oneTimeTokenLogin(Customizer.withDefaults());
  51. return http.build();
  52. }
  53. }
  54. import org.springframework.mail.SimpleMailMessage;
  55. import org.springframework.mail.javamail.JavaMailSender;
  56. @Component <1>
  57. public class MagicLinkGeneratedOneTimeTokenSuccessHandler implements GeneratedOneTimeTokenHandler {
  58. private final MailSender mailSender;
  59. private final GeneratedOneTimeTokenHandler redirectHandler = new RedirectGeneratedOneTimeTokenHandler("/ott/sent");
  60. // constructor omitted
  61. @Override
  62. public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
  63. UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
  64. .replacePath(request.getContextPath())
  65. .replaceQuery(null)
  66. .fragment(null)
  67. .path("/login/ott")
  68. .queryParam("token", oneTimeToken.getTokenValue()); <2>
  69. String magicLink = builder.toUriString();
  70. String email = getUserEmail(oneTimeToken.getUsername()); <3>
  71. this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: " + magicLink); <4>
  72. this.redirectHandler.handle(request, response, oneTimeToken); <5>
  73. }
  74. private String getUserEmail() {
  75. // ...
  76. }
  77. }
  78. @Controller
  79. class PageController {
  80. @GetMapping("/ott/sent")
  81. String ottSent() {
  82. return "my-template";
  83. }
  84. }
  85. ----
  86. Kotlin::
  87. +
  88. [source,kotlin,role="secondary"]
  89. ----
  90. @Configuration
  91. @EnableWebSecurity
  92. class SecurityConfig {
  93. @Bean
  94. open fun filterChain(
  95. http: HttpSecurity,
  96. magicLinkSender: MagicLinkGeneratedOneTimeTokenSuccessHandler?
  97. ): SecurityFilterChain {
  98. http{
  99. formLogin {}
  100. oneTimeTokenLogin { }
  101. }
  102. return http.build()
  103. }
  104. }
  105. import org.springframework.mail.SimpleMailMessage;
  106. import org.springframework.mail.javamail.JavaMailSender;
  107. @Component (1)
  108. class MagicLinkGeneratedOneTimeTokenSuccessHandler(
  109. private val mailSender: MailSender,
  110. private val redirectHandler: GeneratedOneTimeTokenHandler = RedirectGeneratedOneTimeTokenHandler("/ott/sent")
  111. ) : GeneratedOneTimeTokenHandler {
  112. override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
  113. val builder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
  114. .replacePath(request.contextPath)
  115. .replaceQuery(null)
  116. .fragment(null)
  117. .path("/login/ott")
  118. .queryParam("token", oneTimeToken.getTokenValue()) (2)
  119. val magicLink = builder.toUriString()
  120. val email = getUserEmail(oneTimeToken.getUsername()) (3)
  121. this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: $magicLink")(4)
  122. this.redirectHandler.handle(request, response, oneTimeToken) (5)
  123. }
  124. private fun getUserEmail(): String {
  125. // ...
  126. }
  127. }
  128. @Controller
  129. class PageController {
  130. @GetMapping("/ott/sent")
  131. fun ottSent(): String {
  132. return "my-template"
  133. }
  134. }
  135. ----
  136. ======
  137. <1> Make the `MagicLinkGeneratedOneTimeTokenSuccessHandler` a Spring bean
  138. <2> Create a login processing URL with the `token` as a query param
  139. <3> Retrieve the user's email based on the username
  140. <4> Use the `JavaMailSender` API to send the email to the user with the magic link
  141. <5> Use the `RedirectGeneratedOneTimeTokenHandler` to perform a redirect to your desired URL
  142. The email content will look similar to:
  143. > Use the following link to sign in into the application: \http://localhost:8080/login/ott?token=a830c444-29d8-4d98-9b46-6aba7b22fe5b
  144. 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.
  145. [[changing-generate-url]]
  146. == Changing the One-Time Token Generate URL
  147. By default, the javadoc:org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter[] listens to `POST /ott/generate` requests.
  148. That URL can be changed by using the `generateTokenUrl(String)` DSL method:
  149. .Changing the Generate URL
  150. [tabs]
  151. ======
  152. Java::
  153. +
  154. [source,java,role="primary"]
  155. ----
  156. @Configuration
  157. @EnableWebSecurity
  158. public class SecurityConfig {
  159. @Bean
  160. public SecurityFilterChain filterChain(HttpSecurity http) {
  161. http
  162. // ...
  163. .formLogin(Customizer.withDefaults())
  164. .oneTimeTokenLogin((ott) -> ott
  165. .generateTokenUrl("/ott/my-generate-url")
  166. );
  167. return http.build();
  168. }
  169. }
  170. @Component
  171. public class MagicLinkGeneratedOneTimeTokenSuccessHandler implements GeneratedOneTimeTokenHandler {
  172. // ...
  173. }
  174. ----
  175. Kotlin::
  176. +
  177. [source,kotlin,role="secondary"]
  178. ----
  179. @Configuration
  180. @EnableWebSecurity
  181. class SecurityConfig {
  182. @Bean
  183. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  184. http {
  185. //...
  186. formLogin { }
  187. oneTimeTokenLogin {
  188. generateTokenUrl = "/ott/my-generate-url"
  189. }
  190. }
  191. return http.build()
  192. }
  193. }
  194. @Component
  195. class MagicLinkGeneratedOneTimeTokenSuccessHandler : GeneratedOneTimeTokenHandler {
  196. // ...
  197. }
  198. ----
  199. ======
  200. [[changing-submit-page-url]]
  201. == Changing the Default Submit Page URL
  202. 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`.
  203. The URL can also be changed, like so:
  204. .Configuring the Default Submit Page URL
  205. [tabs]
  206. ======
  207. Java::
  208. +
  209. [source,java,role="primary"]
  210. ----
  211. @Configuration
  212. @EnableWebSecurity
  213. public class SecurityConfig {
  214. @Bean
  215. public SecurityFilterChain filterChain(HttpSecurity http) {
  216. http
  217. // ...
  218. .formLogin(Customizer.withDefaults())
  219. .oneTimeTokenLogin((ott) -> ott
  220. .submitPageUrl("/ott/submit")
  221. );
  222. return http.build();
  223. }
  224. }
  225. @Component
  226. public class MagicLinkGeneratedOneTimeTokenSuccessHandler implements GeneratedOneTimeTokenSuccessHandler {
  227. // ...
  228. }
  229. ----
  230. Kotlin::
  231. +
  232. [source,kotlin,role="secondary"]
  233. ----
  234. @Configuration
  235. @EnableWebSecurity
  236. class SecurityConfig {
  237. @Bean
  238. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  239. http {
  240. //...
  241. formLogin { }
  242. oneTimeTokenLogin {
  243. submitPageUrl = "/ott/submit"
  244. }
  245. }
  246. return http.build()
  247. }
  248. }
  249. @Component
  250. class MagicLinkGeneratedOneTimeTokenSuccessHandler : GeneratedOneTimeTokenHandler {
  251. // ...
  252. }
  253. ----
  254. ======
  255. [[disabling-default-submit-page]]
  256. == Disabling the Default Submit Page
  257. If you want to use your own One-Time Token submit page, you can disable the default page and then provide your own endpoint.
  258. .Disabling the Default Submit Page
  259. [tabs]
  260. ======
  261. Java::
  262. +
  263. [source,java,role="primary"]
  264. ----
  265. @Configuration
  266. @EnableWebSecurity
  267. public class SecurityConfig {
  268. @Bean
  269. public SecurityFilterChain filterChain(HttpSecurity http) {
  270. http
  271. .authorizeHttpRequests((authorize) -> authorize
  272. .requestMatchers("/my-ott-submit").permitAll()
  273. .anyRequest().authenticated()
  274. )
  275. .formLogin(Customizer.withDefaults())
  276. .oneTimeTokenLogin((ott) -> ott
  277. .showDefaultSubmitPage(false)
  278. );
  279. return http.build();
  280. }
  281. }
  282. @Controller
  283. public class MyController {
  284. @GetMapping("/my-ott-submit")
  285. public String ottSubmitPage() {
  286. return "my-ott-submit";
  287. }
  288. }
  289. @Component
  290. public class MagicLinkGeneratedOneTimeTokenSuccessHandler implements GeneratedOneTimeTokenSuccessHandler {
  291. // ...
  292. }
  293. ----
  294. Kotlin::
  295. +
  296. [source,kotlin,role="secondary"]
  297. ----
  298. @Configuration
  299. @EnableWebSecurity
  300. class SecurityConfig {
  301. @Bean
  302. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  303. http {
  304. authorizeHttpRequests {
  305. authorize("/my-ott-submit", authenticated)
  306. authorize(anyRequest, authenticated)
  307. }
  308. formLogin { }
  309. oneTimeTokenLogin {
  310. showDefaultSubmitPage = false
  311. }
  312. }
  313. return http.build()
  314. }
  315. }
  316. @Controller
  317. class MyController {
  318. @GetMapping("/my-ott-submit")
  319. fun ottSubmitPage(): String {
  320. return "my-ott-submit"
  321. }
  322. }
  323. @Component
  324. class MagicLinkGeneratedOneTimeTokenSuccessHandler : GeneratedOneTimeTokenHandler {
  325. // ...
  326. }
  327. ----
  328. ======