EmailCodeAuthenticationProvider.kt

package io.github.lishangbu.avalon.auth.authentication

import io.github.lishangbu.avalon.auth.service.VerificationCodeService
import io.github.lishangbu.avalon.oauth2.authorizationserver.authentication.EmailAuthenticationToken
import io.github.lishangbu.avalon.oauth2.authorizationserver.exception.InvalidCaptchaException
import io.github.lishangbu.avalon.oauth2.common.core.AuthorizationGrantTypeSupport
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.stereotype.Component
import java.util.*

/**
 * 邮箱验证码认证提供者
 *
 * 使用 Redis 中的邮箱验证码进行认证
 *
 * @author lishangbu
 * @since 2026/3/13
 */
@Component
class EmailCodeAuthenticationProvider(
    /** 验证码服务 */
    private val verificationCodeService: VerificationCodeService,
    /** 用户详情服务 */
    private val userDetailsService: UserDetailsService,
) : AuthenticationProvider {
    /** 校验邮箱验证码并构造已认证令牌 */
    @Throws(AuthenticationException::class)
    override fun authenticate(authentication: Authentication): Authentication {
        val emailAuthenticationToken = authentication as EmailAuthenticationToken
        val normalizedEmail = normalizeEmail(emailAuthenticationToken.principal)
        val normalizedCode = resolveRequiredText(emailAuthenticationToken.credentials)
        verificationCodeService.verifyCode(
            normalizedEmail,
            normalizedCode,
            AuthorizationGrantTypeSupport.EMAIL.value,
        )
        val userDetails: UserDetails = userDetailsService.loadUserByUsername(normalizedEmail)
        val authenticated =
            EmailAuthenticationToken(userDetails, null, userDetails.authorities).also {
                it.details = authentication.details
            }
        return authenticated
    }

    /** 判断当前提供者是否支持邮箱验证码令牌 */
    override fun supports(authentication: Class<*>): Boolean = EmailAuthenticationToken::class.java.isAssignableFrom(authentication)

    /** 解析必填文本 */
    private fun resolveRequiredText(value: Any?): String =
        value
            ?.toString()
            ?.trim()
            ?.takeIf { it.isNotEmpty() }
            ?: throw InvalidCaptchaException("邮箱验证码不能为空")

    /** 规范化邮箱 */
    private fun normalizeEmail(email: Any?): String = resolveRequiredText(email).lowercase(Locale.ROOT)
}