ワンタイムトークンログイン

Spring Security は、oneTimeTokenLogin() DSL を介してワンタイムトークン (OTT) 認証をサポートします。実装の詳細に入る前に、フレームワーク内の OTT 機能の範囲を明確にし、サポートされている機能とサポートされていない機能を明確にすることが重要です。

ワンタイムトークンとワンタイムパスワードの違いを理解する

ワンタイムトークン (OTT) とワンタイムパスワード [Wikipedia] (英語) (OTP) を混同することはよくありますが、Spring Security では、これらの概念はいくつかの重要な点で異なります。わかりやすくするために、OTP は TOTP [Wikipedia] (英語) (時間ベースのワンタイムパスワード) または HOTP [Wikipedia] (英語) (HMAC ベースのワンタイムパスワード) を指すものとします。

セットアップ要件

  • OTT: 初期設定は必要ありません。ユーザーは事前に何も設定する必要はありません。

  • OTP: 通常、ワンタイムパスワードを生成するには、外部ツールで秘密鍵を生成して共有するなどの設定が必要です。

トークン配信

  • OTT: 通常、エンドユーザーにトークンを配信する責任を負うカスタム OneTimeTokenGenerationSuccessHandler (Javadoc) を実装する必要があります。

  • OTP: トークンは外部ツールによって生成されることが多いため、アプリケーション経由でユーザーに送信する必要はありません。

トークン生成

要約すると、ワンタイムトークン (OTT) は、追加のアカウント設定なしでユーザーを認証する方法を提供します。これは、通常、より複雑な設定プロセスを伴い、トークン生成に外部ツールに依存するワンタイムパスワード (OTP) とは異なります。

ワンタイムトークンログインは、主に 2 つのステップで機能します。

  1. ユーザーは、ユーザー識別子 (通常はユーザー名) を送信してトークンをリクエストし、トークンは多くの場合マジックリンクとしてメール、SMS などを介してユーザーに配信されます。

  2. ユーザーはトークンをワンタイムトークンログインエンドポイントに送信し、有効な場合はユーザーがログインします。

次のセクションでは、ニーズに合わせて 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"
    }
}
1MagicLinkOneTimeTokenGenerationSuccessHandler を Spring Bean にする
2token をクエリパラメーターとしてログイン処理 URL を作成する
3 ユーザー名に基づいてユーザーのメールアドレスを取得する
4JavaMailSender API を使用して、マジックリンクを含むメールをユーザーに送信します。
5RedirectOneTimeTokenGenerationSuccessHandler を使用して、目的の URL へのリダイレクトを実行します。

メールの内容は次のようになります。

アプリケーションにサインインするには、次のリンクを使用してください: http://localhost:8080/login/ott?token=a830c444-29d8-4d98-9b46-6aba7b22fe5b

デフォルトの送信ページでは、URL に token クエリパラメーターがあることが検出され、フォームフィールドにトークン値が自動的に入力されます。

ワンタイムトークン生成 URL の変更

デフォルトでは、GenerateOneTimeTokenFilter (Javadoc) は POST /ott/generate リクエストをリッスンします。その URL は、generateTokenUrl(String) DSL メソッドを使用して変更できます。

生成 URL の変更
  • 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 は次のように変更することもできます。

デフォルトの送信ページ 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 によって自動的に取得できるようにすることです。

OneTimeTokenService を Bean として合格
  • 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 が必要な場合に便利です。

DSL を使用して 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 {
     // ...
}