ワンタイムトークンログイン
Spring Security は、oneTimeTokenLogin()
DSL を介してワンタイムトークン (OTT) 認証をサポートします。実装の詳細に入る前に、フレームワーク内の OTT 機能の範囲を明確にし、サポートされている機能とサポートされていない機能を明確にすることが重要です。
ワンタイムトークンとワンタイムパスワードの違いを理解する
ワンタイムトークン (OTT) とワンタイムパスワード [Wikipedia] (英語) (OTP) を混同することはよくありますが、Spring Security では、これらの概念はいくつかの重要な点で異なります。わかりやすくするために、OTP は TOTP [Wikipedia] (英語) (時間ベースのワンタイムパスワード) または HOTP [Wikipedia] (英語) (HMAC ベースのワンタイムパスワード) を指すものとします。
セットアップ要件
OTT: 初期設定は必要ありません。ユーザーは事前に何も設定する必要はありません。
OTP: 通常、ワンタイムパスワードを生成するには、外部ツールで秘密鍵を生成して共有するなどの設定が必要です。
トークン配信
OTT: 通常、エンドユーザーにトークンを配信する責任を負うカスタム
OneTimeTokenGenerationSuccessHandler
(Javadoc) を実装する必要があります。OTP: トークンは外部ツールによって生成されることが多いため、アプリケーション経由でユーザーに送信する必要はありません。
トークン生成
OTT:
OneTimeTokenService.generate(GenerateOneTimeTokenRequest)
(Javadoc) メソッドでは、サーバー側の生成を重視して、OneTimeToken
(Javadoc) を返す必要があります。OTP: トークンは必ずしもサーバー側で生成されるわけではなく、多くの場合、共有シークレットを使用してクライアントによって作成されます。
要約すると、ワンタイムトークン (OTT) は、追加のアカウント設定なしでユーザーを認証する方法を提供します。これは、通常、より複雑な設定プロセスを伴い、トークン生成に外部ツールに依存するワンタイムパスワード (OTP) とは異なります。
ワンタイムトークンログインは、主に 2 つのステップで機能します。
ユーザーは、ユーザー識別子 (通常はユーザー名) を送信してトークンをリクエストし、トークンは多くの場合マジックリンクとしてメール、SMS などを介してユーザーに配信されます。
ユーザーはトークンをワンタイムトークンログインエンドポイントに送信し、有効な場合はユーザーがログインします。
次のセクションでは、ニーズに合わせて OTT ログインを構成する方法について説明します。
デフォルトのログインページとデフォルトのワンタイムトークン送信ページ
oneTimeTokenLogin()
DSL は formLogin()
と組み合わせて使用することができ、これにより、デフォルトで生成されるログインページに追加のワンタイムトークンリクエストフォームが生成されます。また、DefaultOneTimeTokenSubmitPageGeneratingFilter
(Javadoc) を設定して、デフォルトのワンタイムトークン送信ページを生成します。
トークンをユーザーに送信する
Spring Security では、トークンをユーザーに配信する方法を合理的に決定することはできません。ニーズに基づいてトークンをユーザーに配信するには、カスタム OneTimeTokenGenerationSuccessHandler
(Javadoc) を提供する必要があります。最も一般的な配信戦略の 1 つは、メール、SMS などによるマジックリンクです。次の例では、マジックリンクを作成し、ユーザーのメールに送信します。
Java
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
}
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
@Component (1)
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
private final MailSender mailSender;
private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
// constructor omitted
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue()); (2)
String magicLink = builder.toUriString();
String email = getUserEmail(oneTimeToken.getUsername()); (3)
this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: " + magicLink); (4)
this.redirectHandler.handle(request, response, oneTimeToken); (5)
}
private String getUserEmail() {
// ...
}
}
@Controller
class PageController {
@GetMapping("/ott/sent")
String ottSent() {
return "my-template";
}
}
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http{
formLogin {}
oneTimeTokenLogin { }
}
return http.build()
}
}
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
@Component (1)
class MagicLinkOneTimeTokenGenerationSuccessHandler(
private val mailSender: MailSender,
private val redirectHandler: OneTimeTokenGenerationSuccessHandler = RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
) : OneTimeTokenGenerationSuccessHandler {
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
val builder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.contextPath)
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue()) (2)
val magicLink = builder.toUriString()
val email = getUserEmail(oneTimeToken.getUsername()) (3)
this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: $magicLink")(4)
this.redirectHandler.handle(request, response, oneTimeToken) (5)
}
private fun getUserEmail(): String {
// ...
}
}
@Controller
class PageController {
@GetMapping("/ott/sent")
fun ottSent(): String {
return "my-template"
}
}
1 | MagicLinkOneTimeTokenGenerationSuccessHandler を Spring Bean にする |
2 | token をクエリパラメーターとしてログイン処理 URL を作成する |
3 | ユーザー名に基づいてユーザーのメールアドレスを取得する |
4 | JavaMailSender API を使用して、マジックリンクを含むメールをユーザーに送信します。 |
5 | RedirectOneTimeTokenGenerationSuccessHandler を使用して、目的の URL へのリダイレクトを実行します。 |
メールの内容は次のようになります。
アプリケーションにサインインするには、次のリンクを使用してください: http://localhost:8080/login/ott?token=a830c444-29d8-4d98-9b46-6aba7b22fe5b
デフォルトの送信ページでは、URL に token
クエリパラメーターがあることが検出され、フォームフィールドにトークン値が自動的に入力されます。
ワンタイムトークン生成 URL の変更
デフォルトでは、GenerateOneTimeTokenFilter
(Javadoc) は POST /ott/generate
リクエストをリッスンします。その URL は、generateTokenUrl(String)
DSL メソッドを使用して変更できます。
Java
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.generateTokenUrl("/ott/my-generate-url")
);
return http.build();
}
}
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
generateTokenUrl = "/ott/my-generate-url"
}
}
return http.build()
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}
デフォルトの送信ページ URL の変更
デフォルトのワンタイムトークン送信ページは DefaultOneTimeTokenSubmitPageGeneratingFilter
(Javadoc) によって生成され、GET /login/ott
をリッスンします。URL は次のように変更することもできます。
Java
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.submitPageUrl("/ott/submit")
);
return http.build();
}
}
@Component
public class MagicLinkGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
submitPageUrl = "/ott/submit"
}
}
return http.build()
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}
デフォルトの送信ページを無効にする
独自のワンタイムトークン送信ページを使用する場合は、デフォルトのページを無効にして、独自のエンドポイントを提供できます。
Java
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/my-ott-submit").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.showDefaultSubmitPage(false)
);
return http.build();
}
}
@Controller
public class MyController {
@GetMapping("/my-ott-submit")
public String ottSubmitPage() {
return "my-ott-submit";
}
}
@Component
public class OneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/my-ott-submit", authenticated)
authorize(anyRequest, authenticated)
}
formLogin { }
oneTimeTokenLogin {
showDefaultSubmitPage = false
}
}
return http.build()
}
}
@Controller
class MyController {
@GetMapping("/my-ott-submit")
fun ottSubmitPage(): String {
return "my-ott-submit"
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}
ワンタイムトークンの生成と使用方法をカスタマイズする
ワンタイムトークンを生成および使用するための共通操作を定義するインターフェースは、OneTimeTokenService
(Javadoc) です。Spring Security は、インターフェースが提供されていない場合は、InMemoryOneTimeTokenService
(Javadoc) をそのインターフェースのデフォルト実装として使用します。本番環境では、JdbcOneTimeTokenService
(Javadoc) の使用を検討してください。
OneTimeTokenService
をカスタマイズする最も一般的な理由は次のとおりです (ただし、これらに限定されません)。
ワンタイムトークンの有効期限の変更
トークン生成リクエストからの詳細情報の保存
トークン値の作成方法の変更
ワンタイムトークン使用時の追加の検証
OneTimeTokenService
をカスタマイズするには 2 つのオプションがあります。1 つのオプションは、Bean として提供して、oneTimeTokenLogin()
DSL によって自動的に取得できるようにすることです。
Java
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public OneTimeTokenService oneTimeTokenService() {
return new MyCustomOneTimeTokenService();
}
}
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin { }
}
return http.build()
}
@Bean
open fun oneTimeTokenService(): OneTimeTokenService {
return MyCustomOneTimeTokenService()
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}
2 番目のオプションは、OneTimeTokenService
インスタンスを DSL に渡すことです。これは、複数の SecurityFilterChain
があり、それぞれに異なる OneTimeTokenService
が必要な場合に便利です。
Java
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.oneTimeTokenService(new MyCustomOneTimeTokenService())
);
return http.build();
}
}
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
oneTimeTokenService = MyCustomOneTimeTokenService()
}
}
return http.build()
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}