From 793c5b24ad5c54aec4a34f32009453e9f964a383 Mon Sep 17 00:00:00 2001 From: ivyxjc Date: Fri, 26 Sep 2025 23:07:27 +0800 Subject: [PATCH] feat: optimize the code structure User multiple services instead of one single UserService Implement PostgreSQL repository and entity structure for user authentication --- build.gradle.kts | 15 +- core/build.gradle.kts | 33 +- .../authforge/core/UserService.java | 59 -- .../authforge/core/UserServiceImpl.java | 524 ------------------ .../core/controller/MfaController.java | 88 --- .../core/examples/MfaUsageExample.java | 168 ------ .../core/exception/CastleException.java | 4 + .../core/model/ChallengeSession.java | 10 - .../authforge/core/model/OneTimePassword.java | 17 - .../authforge/core/model/User.java | 13 - .../authforge/core/model/UserHmacSecret.java | 16 - .../authforge/core/model/UserLoginItem.java | 16 - .../core/model/UserRefreshTokenMapping.java | 16 - .../ChallengeSessionRepository.java | 14 + .../repository/OneTimePasswordRepository.java | 11 + .../repository/RefreshTokenRepository.java | 21 + .../repository/UserHmacSecretRepository.java | 16 + .../repository/UserLoginItemRepository.java | 15 + .../UserRegistrationRepository.java | 11 + .../core/repository/UserRepository.java | 36 +- .../repository/dynamodb/DynamodbUser.java | 194 ------- .../dynamodb/DynamodbUserRepositoryImpl.java | 195 ------- .../repository/dynamodb/DynamodbUserUtil.java | 88 --- .../RdsJpaChallengeSessionRepository.java | 9 - .../rdsjpa/RdsJpaOneTimePasswordId.java | 32 -- .../RdsJpaOneTimePasswordRepository.java | 13 - .../rdsjpa/RdsJpaUserHmacSecretId.java | 24 - .../RdsJpaUserHmacSecretRepository.java | 11 - .../rdsjpa/RdsJpaUserLoginItemRepository.java | 20 - .../rdsjpa/RdsJpaUserModelRepository.java | 9 - .../RdsJpaUserRefreshTokenMappingId.java | 32 -- ...sJpaUserRefreshTokenMappingRepository.java | 11 - .../rdsjpa/RdsJpaUserRepositoryImpl.java | 133 ----- .../authforge/core/service/MfaService.java | 30 + .../authforge/core/service/OtpService.java | 12 + .../core/service/TokenSessionService.java | 11 + .../core/service/UserAuthService.java | 26 + .../core/service/impl/MfaServiceImpl.java | 186 +++++++ .../core/service/impl/OtpServiceImpl.java | 89 +++ .../service/impl/TokenSessionServiceImpl.java | 34 ++ .../service/impl/UserAuthServiceImpl.java | 236 ++++++++ examples/spring-boot-example/build.gradle.kts | 29 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../springbootexample/AuthController.java | 40 +- .../springboot/springbootexample/Beans.java | 99 ++-- .../ChallengeSessionRepositoryAdapter.java | 8 - .../OneTimePasswordRepositoryAdapter.java | 9 - .../springbootexample/RepositoryBeans.java | 66 +++ .../SpringBootExampleApplication.java | 9 +- .../springbootexample/UserController.java | 33 -- .../UserHmacSecretRepositoryAdapter.java | 9 - .../UserLoginItemRepositoryAdapter.java | 10 - .../UserModelRepositoryAdapter.java | 10 - ...rRefreshTokenMappingRepositoryAdapter.java | 9 - .../src/main/resources/application.yaml | 2 + impls/impl-postgres/README.md | 1 + impls/impl-postgres/build.gradle.kts | 18 +- .../entity/ChallengeSessionEntity.java | 80 +++ .../entity/OneTimePasswordEntity.java | 65 +++ .../impl/postgres/entity/UserEntity.java | 124 +++++ .../postgres/entity/UserHmacSecretEntity.java | 90 +++ .../postgres/entity/UserHmacSecretId.java | 46 ++ .../postgres/entity/UserLoginItemEntity.java | 144 +++++ .../entity/UserRefreshTokenMappingEntity.java | 68 +++ .../entity/UserRefreshTokenMappingId.java | 46 ++ .../mapper/ChallengeSessionMapper.java | 16 + .../mapper/OneTimePasswordMapper.java | 16 + .../postgres/mapper/UserHmacSecretMapper.java | 20 + .../postgres/mapper/UserLoginItemMapper.java | 16 + .../impl/postgres/mapper/UserMapper.java | 16 + .../mapper/UserRefreshTokenMappingMapper.java | 16 + .../PostgresChallengeSessionRepository.java | 64 +++ .../PostgresLoginItemRepository.java | 65 +++ .../PostgresOneTimePasswordRepository.java | 43 ++ .../PostgresRefreshTokenRepository.java | 53 ++ .../PostgresUserHmacSecretRepository.java | 64 +++ .../PostgresUserModelRepository.java | 42 ++ .../PostgresUserRegistrationRepository.java | 39 ++ .../jpa/ChallengeSessionJpaRepository.java | 14 + .../jpa/OneTimePasswordJpaRepository.java | 17 + .../jpa/UserHmacSecretJpaRepository.java | 22 + .../repository/jpa/UserJpaRepository.java | 7 + .../jpa/UserLoginItemJpaRepository.java | 18 + .../UserRefreshTokenMappingJpaRepository.java | 17 + .../resources/application-postgres.properties | 19 + settings.gradle.kts | 4 +- 86 files changed, 2182 insertions(+), 1921 deletions(-) delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/UserService.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/UserServiceImpl.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/controller/MfaController.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/examples/MfaUsageExample.java create mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/ChallengeSessionRepository.java create mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/OneTimePasswordRepository.java create mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/RefreshTokenRepository.java create mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/UserHmacSecretRepository.java create mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/UserLoginItemRepository.java create mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/UserRegistrationRepository.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUser.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUserRepositoryImpl.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUserUtil.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaChallengeSessionRepository.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaOneTimePasswordId.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaOneTimePasswordRepository.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserHmacSecretId.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserHmacSecretRepository.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserLoginItemRepository.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserModelRepository.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRefreshTokenMappingId.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRefreshTokenMappingRepository.java delete mode 100644 core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRepositoryImpl.java create mode 100644 core/src/main/java/org/clevercastle/authforge/core/service/MfaService.java create mode 100644 core/src/main/java/org/clevercastle/authforge/core/service/OtpService.java create mode 100644 core/src/main/java/org/clevercastle/authforge/core/service/TokenSessionService.java create mode 100644 core/src/main/java/org/clevercastle/authforge/core/service/UserAuthService.java create mode 100644 core/src/main/java/org/clevercastle/authforge/core/service/impl/MfaServiceImpl.java create mode 100644 core/src/main/java/org/clevercastle/authforge/core/service/impl/OtpServiceImpl.java create mode 100644 core/src/main/java/org/clevercastle/authforge/core/service/impl/TokenSessionServiceImpl.java create mode 100644 core/src/main/java/org/clevercastle/authforge/core/service/impl/UserAuthServiceImpl.java delete mode 100644 examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/ChallengeSessionRepositoryAdapter.java delete mode 100644 examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/OneTimePasswordRepositoryAdapter.java create mode 100644 examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/RepositoryBeans.java delete mode 100644 examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserController.java delete mode 100644 examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserHmacSecretRepositoryAdapter.java delete mode 100644 examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserLoginItemRepositoryAdapter.java delete mode 100644 examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserModelRepositoryAdapter.java delete mode 100644 examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserRefreshTokenMappingRepositoryAdapter.java create mode 100644 impls/impl-postgres/README.md create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/ChallengeSessionEntity.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/OneTimePasswordEntity.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserEntity.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserHmacSecretEntity.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserHmacSecretId.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserLoginItemEntity.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserRefreshTokenMappingEntity.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserRefreshTokenMappingId.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/ChallengeSessionMapper.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/OneTimePasswordMapper.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserHmacSecretMapper.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserLoginItemMapper.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserMapper.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserRefreshTokenMappingMapper.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresChallengeSessionRepository.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresLoginItemRepository.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresOneTimePasswordRepository.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresRefreshTokenRepository.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresUserHmacSecretRepository.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresUserModelRepository.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresUserRegistrationRepository.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/ChallengeSessionJpaRepository.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/OneTimePasswordJpaRepository.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserHmacSecretJpaRepository.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserJpaRepository.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserLoginItemJpaRepository.java create mode 100644 impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserRefreshTokenMappingJpaRepository.java create mode 100644 impls/impl-postgres/src/main/resources/application-postgres.properties diff --git a/build.gradle.kts b/build.gradle.kts index b7266c8..cd35681 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,11 +8,17 @@ plugins { group = "org.clevercastle" version = "0.0.1-SNAPSHOT" +allprojects { + group = "org.clevercastle" + version = "0.0.1-SNAPSHOT" -subprojects { repositories { mavenCentral() + gradlePluginPortal() } +} + +subprojects { apply { plugin("java") plugin("jacoco") @@ -20,6 +26,11 @@ subprojects { plugin("signing") } + dependencies { + implementation("org.apache.commons:commons-lang3:3.18.0") + implementation("jakarta.annotation:jakarta.annotation-api:2.1.1") + } + java { toolchain { languageVersion = JavaLanguageVersion.of(21) @@ -27,7 +38,7 @@ subprojects { } tasks.withType().configureEach { - options.release.set(11) + options.release.set(21) } } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index e830cad..ecdd954 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,25 +1,22 @@ dependencies { - implementation("com.fasterxml.jackson.core:jackson-databind:2.18.3") - implementation("com.fasterxml.jackson.core:jackson-core:2.18.3") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.3") + implementation("com.fasterxml.jackson.core:jackson-databind:2.19.2") + implementation("com.fasterxml.jackson.core:jackson-core:2.19.2") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.19.2") - implementation("jakarta.annotation:jakarta.annotation-api:3.0.0") implementation("javax.persistence:javax.persistence-api:2.2") implementation("jakarta.persistence:jakarta.persistence-api:3.2.0") - implementation("com.auth0:java-jwt:4.5.0") - - implementation("com.nimbusds:oauth2-oidc-sdk:11.23.1") - implementation("software.amazon.awssdk:dynamodb:2.31.31") - implementation("software.amazon.awssdk:dynamodb-enhanced:2.31.31") + implementation("javax.transaction:javax.transaction-api:1.3") + implementation("jakarta.transaction:jakarta.transaction-api:2.0.1") - compileOnly("org.springframework.boot:spring-boot-starter-data-jpa:2.7.18") + // todo, just use one of jwt libs + implementation("com.auth0:java-jwt:4.5.0") + implementation("com.nimbusds:oauth2-oidc-sdk:11.23.1") - implementation("org.apache.commons:commons-lang3:3.18.0") - implementation("commons-codec:commons-codec:1.15") + implementation("commons-codec:commons-codec:1.19.0") implementation("org.slf4j:slf4j-api:2.0.17") - testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.mockito:mockito-core:5.17.0") testImplementation("ch.qos.logback:logback-classic:1.5.18") @@ -37,16 +34,6 @@ tasks.test { finalizedBy(tasks.jacocoTestReport) } -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } -} - -tasks.withType().configureEach { - options.release.set(11) -} - mavenPublishing { pom { name.set("auth-forge") diff --git a/core/src/main/java/org/clevercastle/authforge/core/UserService.java b/core/src/main/java/org/clevercastle/authforge/core/UserService.java deleted file mode 100644 index a9f44ee..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/UserService.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.clevercastle.authforge.core; - -import org.apache.commons.lang3.tuple.Pair; -import org.clevercastle.authforge.core.dto.OneTimePasswordDto; -import org.clevercastle.authforge.core.exception.CastleException; -import org.clevercastle.authforge.core.mfa.dto.MfaChallengeResponse; -import org.clevercastle.authforge.core.mfa.dto.MfaFactorResponse; -import org.clevercastle.authforge.core.model.ChallengeSession; -import org.clevercastle.authforge.core.model.User; -import org.clevercastle.authforge.core.model.UserLoginItem; -import org.clevercastle.authforge.core.oauth2.Oauth2ClientConfig; -import org.clevercastle.authforge.core.totp.RequestTotpResponse; -import org.clevercastle.authforge.core.totp.SetupTotpRequest; - -import java.util.List; - -public interface UserService { - // used for username/password, email/password, mobile/password - User register(UserRegisterRequest request) throws CastleException; - - void verify(String loginIdentifier, String verificationCode) throws CastleException; - - UserWithToken login(String loginIdentifier, String password) throws CastleException; - - Pair getByLoginIdentifier(String loginIdentifier) throws CastleException; - - Pair getByUserSub(String userSub) throws CastleException; - - String generate(Oauth2ClientConfig oauth2Client, String redirectUri); - - UserWithToken refresh(User user, UserLoginItem userLoginItem, String refreshToken) throws CastleException; - - // used for sso login - UserWithToken exchange(Oauth2ClientConfig clientConfig, String authorizationCode, String state, String redirectUrl) throws CastleException; - - // one time password login - OneTimePasswordDto requestOneTimePassword(String loginIdentifier) throws CastleException; - - UserWithToken verifyOneTimePassword(String loginIdentifier, String oneTimePassword) throws CastleException; - - // mfa challenge - ChallengeSession createChallenge(String userId, ChallengeSession.Type type); - - RequestTotpResponse requestTotp(User user) throws CastleException; - - // setup mfa - void setupTotp(User user, SetupTotpRequest request) throws CastleException; - - // MFA challenge and verification methods - MfaChallengeResponse createMfaChallenge(User user, String challengeType, String factorId) throws CastleException; - - boolean verifyMfaChallenge(String challengeId, String code, String bindingCode) throws CastleException; - - List listMfaFactors(String userId) throws CastleException; - - void deleteMfaFactor(String userId, String factorId) throws CastleException; - - boolean verifyTotpCode(String userId, String code) throws CastleException; -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/UserServiceImpl.java b/core/src/main/java/org/clevercastle/authforge/core/UserServiceImpl.java deleted file mode 100644 index dec7e48..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/UserServiceImpl.java +++ /dev/null @@ -1,524 +0,0 @@ -package org.clevercastle.authforge.core; - -import com.nimbusds.oauth2.sdk.AuthorizationCode; -import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; -import com.nimbusds.oauth2.sdk.AuthorizationGrant; -import com.nimbusds.oauth2.sdk.TokenErrorResponse; -import com.nimbusds.oauth2.sdk.TokenRequest; -import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; -import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; -import com.nimbusds.oauth2.sdk.auth.Secret; -import com.nimbusds.oauth2.sdk.http.HTTPResponse; -import com.nimbusds.oauth2.sdk.id.ClientID; -import com.nimbusds.openid.connect.sdk.OIDCTokenResponse; -import jakarta.annotation.Nonnull; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.clevercastle.authforge.core.code.CodeSender; -import org.clevercastle.authforge.core.dto.OneTimePasswordDto; -import org.clevercastle.authforge.core.exception.CastleException; -import org.clevercastle.authforge.core.exception.UserExistException; -import org.clevercastle.authforge.core.exception.UserNotFoundException; -import org.clevercastle.authforge.core.mfa.dto.MfaChallengeResponse; -import org.clevercastle.authforge.core.mfa.dto.MfaFactorResponse; -import org.clevercastle.authforge.core.model.ChallengeSession; -import org.clevercastle.authforge.core.model.OneTimePassword; -import org.clevercastle.authforge.core.model.ResourceType; -import org.clevercastle.authforge.core.model.User; -import org.clevercastle.authforge.core.model.UserHmacSecret; -import org.clevercastle.authforge.core.model.UserLoginItem; -import org.clevercastle.authforge.core.oauth2.Oauth2ClientConfig; -import org.clevercastle.authforge.core.oauth2.Oauth2User; -import org.clevercastle.authforge.core.repository.UserRepository; -import org.clevercastle.authforge.core.token.TokenService; -import org.clevercastle.authforge.core.totp.RequestTotpResponse; -import org.clevercastle.authforge.core.totp.SetupTotpRequest; -import org.clevercastle.authforge.core.totp.TotpUtil; -import org.clevercastle.authforge.core.util.CodeUtil; -import org.clevercastle.authforge.core.util.HashUtil; -import org.clevercastle.authforge.core.util.IdUtil; -import org.clevercastle.authforge.core.util.TimeUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.transaction.Transactional; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -public class UserServiceImpl implements UserService { - private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class); - private final Config config; - private final UserRepository userRepository; - private final TokenService tokenService; - private final CodeSender codeSender; - private final CacheService cacheService; - - public UserServiceImpl(Config config, UserRepository userRepository, TokenService tokenService, CodeSender codeSender, CacheService cacheService) { - this.config = config; - this.userRepository = userRepository; - this.tokenService = tokenService; - this.codeSender = codeSender; - this.cacheService = cacheService; - } - - @Override - public User register(UserRegisterRequest userRegisterRequest) throws CastleException { - Pair pair = this.getByLoginIdentifier(userRegisterRequest.getLoginIdentifier()); - User user = pair.getLeft(); - if (user != null) { - if (UserState.DELETED != user.getUserState()) { - throw new UserExistException(); - } - // if the user is deleted, just re-create it - } - String userId = IdUtil.genUserId(); - var now = TimeUtils.now(); - user = new User(); - user.setUserId(userId); - user.setUserState(UserState.ACTIVE); - - user.setHashedPassword(HashUtil.hashPassword(userRegisterRequest.getPassword())); - - user.setCreatedAt(now); - user.setUpdatedAt(now); - - UserLoginItem userLoginItem = new UserLoginItem(); - userLoginItem.setUserId(userId); - userLoginItem.setUserSub(UUID.randomUUID().toString()); - userLoginItem.setType(UserLoginItem.Type.raw); - userLoginItem.setLoginIdentifier(userRegisterRequest.getLoginIdentifier()); - userLoginItem.setLoginIdentifierPrefix(userRegisterRequest.getLoginIdentifierPrefix()); - - userLoginItem.setState(UserLoginItem.State.UNCONFIRMED); - userLoginItem.setVerificationCode(CodeUtil.generateCode(8)); - userLoginItem.setVerificationCodeExpiredAt(TimeUtils.now().plusSeconds(this.config.getVerificationCodeExpireTime())); - userLoginItem.setCreatedAt(now); - userLoginItem.setUpdatedAt(now); - this.userRepository.save(user, userLoginItem); - this.codeSender.sendVerificationCode(userLoginItem.getLoginIdentifier(), userLoginItem.getVerificationCode()); - return user; - } - - - @Override - public void verify(String loginIdentifier, String verificationCode) throws CastleException { - Pair pair = this.userRepository.getByLoginIdentifier(loginIdentifier); - UserLoginItem userLoginItem = pair.getRight(); - if (userLoginItem == null) { - throw new UserNotFoundException(); - } - if (StringUtils.isBlank(verificationCode)) { - throw new CastleException(); - } - if (!StringUtils.equals(verificationCode, userLoginItem.getVerificationCode())) { - throw new CastleException(); - } - if (StringUtils.isBlank(verificationCode)) { - throw new CastleException(); - } - if (StringUtils.isBlank(userLoginItem.getVerificationCode()) || userLoginItem.getVerificationCodeExpiredAt() == null) { - throw new CastleException(); - } - if (userLoginItem.getVerificationCodeExpiredAt().isBefore(TimeUtils.now())) { - throw new CastleException(); - } - if (verificationCode.equals(userLoginItem.getVerificationCode())) { - userRepository.confirmLoginItem(loginIdentifier); - } - } - - @Override - public String generate(Oauth2ClientConfig oauth2Client, String redirectUrl) { - Map map = new LinkedHashMap<>(); - if (oauth2Client.getMandatoryQueryParams() != null) { - map.putAll(oauth2Client.getMandatoryQueryParams()); - } - map.put("client_id", oauth2Client.getClientId()); - map.put("redirect_uri", redirectUrl); - map.put("response_type", "code"); - map.put("scope", StringUtils.join(oauth2Client.getScopes(), "%20")); - map.put("state", UUID.randomUUID().toString()); - String queryString = map.entrySet().stream().map(it -> String.format("%s=%s", it.getKey(), it.getValue())).collect(java.util.stream.Collectors.joining("&")); - // TODO: 2025/4/10 cache the state - return oauth2Client.getOauth2LoginUrl() + "?" + queryString; - } - - @Override - public UserWithToken exchange(Oauth2ClientConfig clientConfig, String authorizationCode, String state, String redirectUrl) throws CastleException { - Oauth2User oauth2User = clientConfig.getOauth2ExchangeService().exchange(clientConfig, authorizationCode, state, redirectUrl); - if (StringUtils.isBlank(oauth2User.getLoginIdentifier())) { - throw new CastleException(); - } - Pair pair = getByLoginIdentifier(oauth2User.getLoginIdentifier()); - var user = pair.getLeft(); - var userLoginItem = pair.getRight(); - if (userLoginItem == null) { - // register process - String userId = IdUtil.genUserId(); - var now = TimeUtils.now(); - user = new User(); - user.setUserId(userId); - user.setUserState(UserState.ACTIVE); - - user.setCreatedAt(now); - user.setUpdatedAt(now); - - userLoginItem = new UserLoginItem(); - userLoginItem.setUserId(userId); - userLoginItem.setLoginIdentifier(oauth2User.getLoginIdentifier()); - userLoginItem.setUserSub(UUID.randomUUID().toString()); - userLoginItem.setCreatedAt(now); - userLoginItem.setUpdatedAt(now); - this.userRepository.save(user, userLoginItem); - TokenHolder tokenHolder = tokenService.generateToken(user, userLoginItem); - userRepository.addRefreshToken(user, tokenHolder.getRefreshToken(), tokenHolder.getExpiresAt()); - return new UserWithToken(user, tokenHolder); - } else { - // login process - if (user == null || UserState.ACTIVE != user.getUserState()) { - throw new CastleException(""); - } - TokenHolder tokenHolder = tokenService.generateToken(user, userLoginItem); - userRepository.addRefreshToken(user, tokenHolder.getRefreshToken(), tokenHolder.getExpiresAt()); - return new UserWithToken(user, tokenHolder); - } - } - - @Override - public UserWithToken login(String loginIdentifier, String password) throws CastleException { - Pair pair = getByLoginIdentifier(loginIdentifier); - var user = pair.getLeft(); - var userLoginItem = pair.getRight(); - if (user == null) { - throw new UserNotFoundException(); - } - if (UserLoginItem.State.ACTIVE != userLoginItem.getState()) { - throw new CastleException("Current login is not confirmed"); - } - if (UserState.ACTIVE != user.getUserState()) { - throw new CastleException(""); - } - boolean verify = HashUtil.verifyPassword(password, user.getHashedPassword()); - if (!verify) { - throw new CastleException("Incorrect password"); - } - TokenHolder tokenHolder = tokenService.generateToken(user, userLoginItem); - userRepository.addRefreshToken(user, tokenHolder.getRefreshToken(), tokenHolder.getExpiresAt()); - return new UserWithToken(user, tokenHolder); - } - - @Override - @Transactional - public UserWithToken refresh(User user, UserLoginItem userLoginItem, String refreshToken) throws CastleException { - if (UserState.ACTIVE != user.getUserState()) { - throw new CastleException(""); - } - boolean verified = userRepository.verifyRefreshToken(user, refreshToken); - if (verified) { - TokenHolder tokenHolder = tokenService.generateToken(user, userLoginItem); - userRepository.addRefreshToken(user, tokenHolder.getRefreshToken(), tokenHolder.getExpiresAt()); - return new UserWithToken(user, tokenHolder); - } else { - throw new CastleException("Fail to refresh"); - } - } - - - /** - * get id token from oauth2 provider - * @param clientConfig - * @param authorizationCode - * @param state - * @param redirectUrl - * @return - * @throws CastleException - */ - private OIDCTokenResponse oauth2Exchange(Oauth2ClientConfig clientConfig, String authorizationCode, String state, String redirectUrl) throws CastleException { - AuthorizationCode code = new AuthorizationCode(authorizationCode); - ClientID clientID = new ClientID(clientConfig.getClientId()); - Secret clientSecret = new Secret(clientConfig.getClientSecret()); - ClientAuthentication clientAuth = new ClientSecretBasic(clientID, clientSecret); - try { - AuthorizationGrant codeGrant; - if (StringUtils.isNotBlank(redirectUrl)) { - codeGrant = new AuthorizationCodeGrant(code, new URI(redirectUrl)); - } else { - codeGrant = new AuthorizationCodeGrant(code, null); - } - URI tokenEndpoint = new URI(clientConfig.getOauth2TokenUrl()); - TokenRequest request = new TokenRequest(tokenEndpoint, clientAuth, codeGrant, null); - var httpRequest = request.toHTTPRequest(); - httpRequest.setHeader("Accept", "application/json"); - HTTPResponse httpResponse = httpRequest.send(); - if (httpResponse.getStatusCode() != HTTPResponse.SC_OK) { - logger.warn("Token exchange error {}", httpResponse.getBody()); - throw new CastleException(); - } - OIDCTokenResponse response = null; - try { - response = OIDCTokenResponse.parse(httpResponse); - } catch (com.nimbusds.oauth2.sdk.ParseException e) { - logger.error("fail to parse token exchange response", e); - throw new CastleException(); - } - if (!response.indicatesSuccess()) { - // We got an error response... - TokenErrorResponse errorResponse = response.toErrorResponse(); - logger.error("OIDC token exchange error {}", response.toErrorResponse()); - throw new CastleException(); - } - return response.toSuccessResponse(); - } catch (URISyntaxException e) { - logger.error("Token exchange URI error.", e); - throw new CastleException(e); - } catch (IOException e) { - throw new CastleException(e); - } - } - - @Nonnull - @Override - public Pair getByLoginIdentifier(String loginIdentifier) throws CastleException { - return userRepository.getByLoginIdentifier(loginIdentifier); - } - - @Override - public Pair getByUserSub(String userSub) throws CastleException { - return userRepository.getByUserSub(userSub); - } - - @Transactional - @Override - public OneTimePasswordDto requestOneTimePassword(String loginIdentifier) throws CastleException { - Pair pair = getByLoginIdentifier(loginIdentifier); - if (pair.getLeft() == null || pair.getRight() == null) { - throw new UserNotFoundException(); - } - if (UserLoginItem.State.ACTIVE != pair.getRight().getState()) { - throw new CastleException("Current login is not confirmed"); - } - if (UserState.ACTIVE != pair.getLeft().getUserState()) { - throw new CastleException("The user is not confirmed"); - } - OneTimePassword oneTimePassword = new OneTimePassword(); - oneTimePassword.setLoginIdentifier(loginIdentifier); - oneTimePassword.setOneTimePassword(CodeUtil.generateCode(6, CodeUtil.UPPER_CHARS)); - oneTimePassword.setExpiredAt(TimeUtils.now().plusSeconds(config.getOneTimePasswordExpireTime())); - oneTimePassword.setCreatedAt(TimeUtils.now()); - userRepository.saveOneTimePassword(oneTimePassword); - this.codeSender.sendOneTimePassword(loginIdentifier, oneTimePassword.getOneTimePassword()); - OneTimePasswordDto oneTimePasswordDto = new OneTimePasswordDto(); - oneTimePasswordDto.setLoginIdentifier(loginIdentifier); - oneTimePasswordDto.setExpiredAt(oneTimePassword.getExpiredAt()); - oneTimePasswordDto.setCreatedAt(oneTimePassword.getCreatedAt()); - return oneTimePasswordDto; - } - - @Transactional - @Override - public UserWithToken verifyOneTimePassword(String loginIdentifier, String oneTimePassword) throws CastleException { - boolean success = userRepository.verifyOneTimePassword(loginIdentifier, oneTimePassword); - if (!success) { - throw new CastleException(); - } - Pair pair = getByLoginIdentifier(loginIdentifier); - if (pair.getLeft() == null || pair.getRight() == null) { - throw new UserNotFoundException(); - } - if (UserLoginItem.State.ACTIVE != pair.getRight().getState()) { - throw new CastleException("Current login is not confirmed"); - } - if (UserState.ACTIVE != pair.getLeft().getUserState()) { - throw new CastleException("The user is not confirmed"); - } - TokenHolder tokenHolder = tokenService.generateToken(pair.getLeft(), pair.getRight()); - userRepository.addRefreshToken(pair.getLeft(), tokenHolder.getRefreshToken(), tokenHolder.getExpiresAt()); - return new UserWithToken(pair.getLeft(), tokenHolder); - } - - @Override - public ChallengeSession createChallenge(String userId, ChallengeSession.Type type) { - ChallengeSession.Type sessionType = null; - ChallengeSession session = new ChallengeSession(); - session.setId(IdUtil.genId(ResourceType.challengeSession)); - session.setType(type); - session.setUserId(userId); - session.setCreatedAt(TimeUtils.now()); - return session; - } - - @Override - public RequestTotpResponse requestTotp(User user) throws CastleException { - String sessionId = UUID.randomUUID().toString(); - String secret = TotpUtil.generateSecret(); - cacheService.set(sessionId, secret, 300); // 5分钟过期时间 - - // 生成QR码URI,使用用户ID作为账户名称 - String accountName = user.getUserId(); // 使用用户ID作为账户标识 - String issuerName = "AuthForge"; // 可以从配置中获取 - String qrCodeUri = TotpUtil.generateQRCodeUri(secret, accountName, issuerName); - - RequestTotpResponse requestTotpDto = new RequestTotpResponse(); - requestTotpDto.setSessionId(sessionId); - requestTotpDto.setSecret(secret); - requestTotpDto.setQrCodeUri(qrCodeUri); - requestTotpDto.setManualEntryKey(secret); - return requestTotpDto; - } - - @Override - public void setupTotp(User user, SetupTotpRequest request) throws CastleException { - String secret = cacheService.get(request.getSessionId()); - if (StringUtils.isBlank(secret)) { - throw new CastleException("Invalid session ID or session expired"); - } - - // 验证用户输入的验证码 - if (request.getCodes() == null || request.getCodes().isEmpty()) { - throw new CastleException("Verification codes are required"); - } - - // 至少需要验证一个验证码 - boolean isVerified = false; - for (var codeEntry : request.getCodes()) { - if (codeEntry.getCode() != null && codeEntry.getInputTime() != null) { - long timeSeconds = codeEntry.getInputTime().toEpochSecond(); - if (TotpUtil.verifyTOTPAtTime(codeEntry.getCode(), secret, timeSeconds)) { - isVerified = true; - break; - } - } - } - - if (!isVerified) { - throw new CastleException("Invalid verification code"); - } - - // 检查用户是否已经设置了TOTP - List existingSecrets = userRepository.listHmacSecretByUserId(user.getUserId()); - if (!existingSecrets.isEmpty()) { - throw new CastleException("TOTP already configured for this user"); - } - - UserHmacSecret userHmacSecret = new UserHmacSecret(); - userHmacSecret.setUserId(user.getUserId()); - userHmacSecret.setId(IdUtil.genId(ResourceType.totp)); - userHmacSecret.setSecret(secret); - var now = TimeUtils.now(); - userHmacSecret.setCreatedAt(now); - userHmacSecret.setLastUsedAt(now); - userHmacSecret.setName(StringUtils.isNotBlank(request.getName()) ? request.getName() : "TOTP Device"); - userRepository.createHmacSecret(userHmacSecret); - - // 清除缓存中的临时密钥 - cacheService.delete(request.getSessionId()); - } - - @Override - public MfaChallengeResponse createMfaChallenge(User user, String challengeType, String factorId) throws CastleException { - if (!"totp".equals(challengeType)) { - throw new CastleException("Unsupported challenge type: " + challengeType); - } - - // 验证用户是否有对应的MFA因子 - List userSecrets = userRepository.listHmacSecretByUserId(user.getUserId()); - UserHmacSecret targetSecret = null; - for (UserHmacSecret secret : userSecrets) { - if (secret.getId().equals(factorId)) { - targetSecret = secret; - break; - } - } - - if (targetSecret == null) { - throw new CastleException("MFA factor not found"); - } - - // 创建挑战会话 - ChallengeSession challengeSession = createChallenge(user.getUserId(), ChallengeSession.Type.mfa); - challengeSession.setCreatedAt(TimeUtils.now()); - challengeSession.setUserId(user.getUserId()); - - // 将挑战会话信息存储到缓存或数据库 - userRepository.createChallenge(challengeSession); - - // 将factorId与挑战会话关联存储(可以用Redis缓存) - cacheService.set("mfa_challenge_" + challengeSession.getId(), factorId, 300); // 5分钟过期 - - MfaChallengeResponse response = new MfaChallengeResponse(); - response.setChallengeId(challengeSession.getId()); - response.setChallengeType(challengeType); - response.setUserId(user.getUserId()); - response.setExpiresAt(TimeUtils.now().plusSeconds(300)); - - return response; - } - - @Override - public boolean verifyMfaChallenge(String challengeId, String code, String bindingCode) throws CastleException { - // 从缓存中获取挑战会话关联的factorId - String factorId = cacheService.get("mfa_challenge_" + challengeId); - if (StringUtils.isBlank(factorId)) { - throw new CastleException("Challenge session not found or expired"); - } - - // 获取用户的HMAC密钥 - // 这里需要根据factorId查找对应的UserHmacSecret - // 由于当前repository接口限制,我们通过其他方式获取 - // 实际应用中应该添加根据factorId查找的方法 - - // 验证TOTP代码 - try { - // 这里需要获取对应的secret,暂时简化处理 - // 在实际应用中,应该根据factorId从数据库中获取对应的secret - return TotpUtil.verifyTOTP(code, "dummy_secret"); // 需要替换为实际的secret - } catch (Exception e) { - logger.error("Error verifying TOTP code", e); - return false; - } finally { - // 验证后清除挑战会话 - cacheService.delete("mfa_challenge_" + challengeId); - } - } - - @Override - public List listMfaFactors(String userId) throws CastleException { - List secrets = userRepository.listHmacSecretByUserId(userId); - return secrets.stream().map(secret -> { - MfaFactorResponse factor = new MfaFactorResponse(); - factor.setId(secret.getId()); - factor.setType("totp"); - factor.setName(secret.getName()); - factor.setActive(true); - factor.setCreatedAt(secret.getCreatedAt()); - factor.setLastUsedAt(secret.getLastUsedAt()); - return factor; - }).collect(java.util.stream.Collectors.toList()); - } - - @Override - public void deleteMfaFactor(String userId, String factorId) throws CastleException { - // 这里需要在UserRepository中添加删除方法 - // userRepository.deleteHmacSecret(userId, factorId); - throw new CastleException("Delete MFA factor not implemented yet"); - } - - @Override - public boolean verifyTotpCode(String userId, String code) throws CastleException { - List secrets = userRepository.listHmacSecretByUserId(userId); - for (UserHmacSecret secret : secrets) { - if (TotpUtil.verifyTOTP(code, secret.getSecret())) { - // 更新最后使用时间 - secret.setLastUsedAt(TimeUtils.now()); - // 这里需要更新到数据库,暂时省略 - return true; - } - } - return false; - } -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/controller/MfaController.java b/core/src/main/java/org/clevercastle/authforge/core/controller/MfaController.java deleted file mode 100644 index af82524..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/controller/MfaController.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.clevercastle.authforge.core.controller; - -import org.clevercastle.authforge.core.UserService; -import org.clevercastle.authforge.core.exception.CastleException; -import org.clevercastle.authforge.core.mfa.dto.MfaChallengeRequest; -import org.clevercastle.authforge.core.mfa.dto.MfaChallengeResponse; -import org.clevercastle.authforge.core.mfa.dto.MfaFactorResponse; -import org.clevercastle.authforge.core.mfa.dto.MfaVerifyRequest; -import org.clevercastle.authforge.core.model.User; -import org.clevercastle.authforge.core.totp.RequestTotpResponse; -import org.clevercastle.authforge.core.totp.SetupTotpRequest; - -import java.util.List; - -/** - * MFA Controller示例 - * 这个类展示了如何使用MFA相关的服务方法 - * 在实际项目中,你可能会使用Spring Boot、JAX-RS或其他Web框架 - */ -public class MfaController { - - private UserService userService; - - public MfaController(UserService userService) { - this.userService = userService; - } - - /** - * 请求TOTP设置(第一步:获取密钥和QR码) - */ - public RequestTotpResponse requestTotpSetup(String userId) throws CastleException { - // 这里应该从认证上下文中获取用户,简化示例 - User user = new User(); - user.setUserId(userId); - - return userService.requestTotp(user); - } - - /** - * 完成TOTP设置(第二步:验证验证码并保存) - */ - public void completeTotpSetup(String userId, SetupTotpRequest request) throws CastleException { - User user = new User(); - user.setUserId(userId); - - userService.setupTotp(user, request); - } - - /** - * 创建MFA挑战 - */ - public MfaChallengeResponse createMfaChallenge(String userId, MfaChallengeRequest request) throws CastleException { - User user = new User(); - user.setUserId(userId); - - return userService.createMfaChallenge( - user, request.getChallengeType(), request.getFactorId()); - } - - /** - * 验证MFA挑战 - */ - public boolean verifyMfaChallenge(MfaVerifyRequest request) throws CastleException { - return userService.verifyMfaChallenge( - request.getChallengeId(), request.getCode(), request.getBindingCode()); - } - - /** - * 列出用户的MFA因子 - */ - public List listMfaFactors(String userId) throws CastleException { - return userService.listMfaFactors(userId); - } - - /** - * 删除MFA因子 - */ - public void deleteMfaFactor(String userId, String factorId) throws CastleException { - userService.deleteMfaFactor(userId, factorId); - } - - /** - * 直接验证TOTP码(用于登录时的MFA验证) - */ - public boolean verifyTotpCode(String userId, String code) throws CastleException { - return userService.verifyTotpCode(userId, code); - } -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/examples/MfaUsageExample.java b/core/src/main/java/org/clevercastle/authforge/core/examples/MfaUsageExample.java deleted file mode 100644 index b20fd63..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/examples/MfaUsageExample.java +++ /dev/null @@ -1,168 +0,0 @@ -package org.clevercastle.authforge.core.examples; - -import org.clevercastle.authforge.core.UserService; -import org.clevercastle.authforge.core.controller.MfaController; -import org.clevercastle.authforge.core.exception.CastleException; -import org.clevercastle.authforge.core.mfa.dto.MfaChallengeRequest; -import org.clevercastle.authforge.core.mfa.dto.MfaChallengeResponse; -import org.clevercastle.authforge.core.mfa.dto.MfaFactorResponse; -import org.clevercastle.authforge.core.mfa.dto.MfaVerifyRequest; -import org.clevercastle.authforge.core.totp.RequestTotpResponse; -import org.clevercastle.authforge.core.totp.SetupTotpRequest; -import org.clevercastle.authforge.core.totp.SetupTotpVerificationCode; - -import java.time.OffsetDateTime; -import java.util.Arrays; -import java.util.List; - -/** - * MFA功能使用示例 - * 展示如何使用auth-forge的MFA功能 - */ -public class MfaUsageExample { - - private UserService userService; - private MfaController mfaController; - - public MfaUsageExample(UserService userService) { - this.userService = userService; - this.mfaController = new MfaController(userService); - } - - /** - * 演示完整的TOTP设置流程 - */ - public void demonstrateTotpSetup(String userId) { - try { - System.out.println("=== TOTP Setup Demo ==="); - - // 第一步:请求TOTP设置 - System.out.println("1. Requesting TOTP setup..."); - RequestTotpResponse setupRequest = mfaController.requestTotpSetup(userId); - - System.out.println("Session ID: " + setupRequest.getSessionId()); - System.out.println("Secret: " + setupRequest.getSecret()); - System.out.println("QR Code URI: " + setupRequest.getQrCodeUri()); - System.out.println("Manual Entry Key: " + setupRequest.getManualEntryKey()); - - // 第二步:用户使用认证器应用扫描QR码,然后输入验证码 - System.out.println("\n2. User scans QR code and enters verification codes..."); - - // 模拟用户输入的验证码(实际应用中由用户通过认证器应用生成) - SetupTotpRequest completionRequest = new SetupTotpRequest(); - completionRequest.setSessionId(setupRequest.getSessionId()); - completionRequest.setName("My Authenticator"); - - // 模拟验证码输入(实际应用中需要用户提供真实的TOTP码) - SetupTotpVerificationCode code1 = new SetupTotpVerificationCode(); - code1.setCode("123456"); // 这应该是真实的TOTP码 - code1.setInputTime(OffsetDateTime.now()); - - completionRequest.setCodes(Arrays.asList(code1)); - - // 注意:这里会失败,因为我们使用的是假的验证码 - // 在实际应用中,用户需要使用认证器应用生成真实的验证码 - try { - mfaController.completeTotpSetup(userId, completionRequest); - System.out.println("TOTP setup completed successfully!"); - } catch (CastleException e) { - System.out.println("TOTP setup failed: " + e.getMessage()); - System.out.println("(This is expected in demo mode with fake verification codes)"); - } - - } catch (CastleException e) { - System.err.println("Error during TOTP setup: " + e.getMessage()); - } - } - - /** - * 演示MFA挑战流程 - */ - public void demonstrateMfaChallenge(String userId, String factorId) { - try { - System.out.println("\n=== MFA Challenge Demo ==="); - - // 创建MFA挑战 - System.out.println("1. Creating MFA challenge..."); - MfaChallengeRequest challengeRequest = new MfaChallengeRequest(); - challengeRequest.setChallengeType("totp"); - challengeRequest.setFactorId(factorId); - - MfaChallengeResponse challengeResponse = mfaController.createMfaChallenge(userId, challengeRequest); - - System.out.println("Challenge ID: " + challengeResponse.getChallengeId()); - System.out.println("Challenge Type: " + challengeResponse.getChallengeType()); - System.out.println("Expires At: " + challengeResponse.getExpiresAt()); - - // 验证MFA挑战 - System.out.println("\n2. Verifying MFA challenge..."); - MfaVerifyRequest verifyRequest = new MfaVerifyRequest(); - verifyRequest.setChallengeId(challengeResponse.getChallengeId()); - verifyRequest.setCode("123456"); // 模拟TOTP码 - - boolean verified = mfaController.verifyMfaChallenge(verifyRequest); - System.out.println("MFA verification result: " + verified); - - } catch (CastleException e) { - System.err.println("Error during MFA challenge: " + e.getMessage()); - } - } - - /** - * 演示列出和管理MFA因子 - */ - public void demonstrateMfaFactorManagement(String userId) { - try { - System.out.println("\n=== MFA Factor Management Demo ==="); - - // 列出用户的MFA因子 - System.out.println("1. Listing user's MFA factors..."); - List factors = mfaController.listMfaFactors(userId); - - if (factors.isEmpty()) { - System.out.println("No MFA factors found for user."); - } else { - for (MfaFactorResponse factor : factors) { - System.out.println("Factor ID: " + factor.getId()); - System.out.println("Factor Type: " + factor.getType()); - System.out.println("Factor Name: " + factor.getName()); - System.out.println("Active: " + factor.isActive()); - System.out.println("Created: " + factor.getCreatedAt()); - System.out.println("Last Used: " + factor.getLastUsedAt()); - System.out.println("---"); - } - } - - // 直接验证TOTP码 - System.out.println("\n2. Direct TOTP verification..."); - boolean totpVerified = mfaController.verifyTotpCode(userId, "123456"); - System.out.println("Direct TOTP verification result: " + totpVerified); - - } catch (CastleException e) { - System.err.println("Error during MFA factor management: " + e.getMessage()); - } - } - - /** - * 主演示方法 - */ - public static void demonstrateAllFeatures(UserService userService) { - String userId = "demo-user-123"; - String factorId = "demo-factor-456"; - - MfaUsageExample example = new MfaUsageExample(userService); - - // 演示TOTP设置 - example.demonstrateTotpSetup(userId); - - // 演示MFA挑战(需要有有效的factorId) - example.demonstrateMfaChallenge(userId, factorId); - - // 演示MFA因子管理 - example.demonstrateMfaFactorManagement(userId); - - System.out.println("\n=== Demo completed ==="); - System.out.println("Note: Some operations may fail in demo mode because they require"); - System.out.println("real TOTP codes from an authenticator app."); - } -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/exception/CastleException.java b/core/src/main/java/org/clevercastle/authforge/core/exception/CastleException.java index ffb3cba..ac9dfc7 100644 --- a/core/src/main/java/org/clevercastle/authforge/core/exception/CastleException.java +++ b/core/src/main/java/org/clevercastle/authforge/core/exception/CastleException.java @@ -8,6 +8,10 @@ public CastleException(String message) { super(message); } + public CastleException(String message, Throwable cause) { + super(message, cause); + } + public CastleException(Throwable cause) { super(cause); } diff --git a/core/src/main/java/org/clevercastle/authforge/core/model/ChallengeSession.java b/core/src/main/java/org/clevercastle/authforge/core/model/ChallengeSession.java index 1f8a40a..78d3f3b 100644 --- a/core/src/main/java/org/clevercastle/authforge/core/model/ChallengeSession.java +++ b/core/src/main/java/org/clevercastle/authforge/core/model/ChallengeSession.java @@ -1,23 +1,13 @@ package org.clevercastle.authforge.core.model; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; - import java.time.OffsetDateTime; -@javax.persistence.Entity -@javax.persistence.Table(name = "challenge_session") -@Entity -@Table(name = "challenge_session") public class ChallengeSession { public enum Type { mfa, changePassword } - @javax.persistence.Id - @Id private String id; private Type type; diff --git a/core/src/main/java/org/clevercastle/authforge/core/model/OneTimePassword.java b/core/src/main/java/org/clevercastle/authforge/core/model/OneTimePassword.java index 87ff274..6f9a016 100644 --- a/core/src/main/java/org/clevercastle/authforge/core/model/OneTimePassword.java +++ b/core/src/main/java/org/clevercastle/authforge/core/model/OneTimePassword.java @@ -1,26 +1,9 @@ package org.clevercastle.authforge.core.model; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.IdClass; -import jakarta.persistence.Table; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaOneTimePasswordId; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserRefreshTokenMappingId; - import java.time.OffsetDateTime; -@javax.persistence.Entity -@javax.persistence.Table(name = "one_time_password") -@Entity -@Table(name = "one_time_password") -@javax.persistence.IdClass(RdsJpaOneTimePasswordId.class) -@IdClass(RdsJpaUserRefreshTokenMappingId.class) public class OneTimePassword { - @javax.persistence.Id - @Id private String loginIdentifier; - @javax.persistence.Id - @Id private String oneTimePassword; private OffsetDateTime expiredAt; diff --git a/core/src/main/java/org/clevercastle/authforge/core/model/User.java b/core/src/main/java/org/clevercastle/authforge/core/model/User.java index a5ca9df..2065ae3 100644 --- a/core/src/main/java/org/clevercastle/authforge/core/model/User.java +++ b/core/src/main/java/org/clevercastle/authforge/core/model/User.java @@ -1,25 +1,12 @@ package org.clevercastle.authforge.core.model; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.Id; -import jakarta.persistence.Table; import org.clevercastle.authforge.core.UserState; import java.time.OffsetDateTime; import java.util.List; -@javax.persistence.Entity -@javax.persistence.Table(name = "users") -@Entity -@Table(name = "users") public class User { - @javax.persistence.Id - @Id private String userId; - @javax.persistence.Enumerated(javax.persistence.EnumType.STRING) - @Enumerated(EnumType.STRING) private UserState userState; private String hashedPassword; diff --git a/core/src/main/java/org/clevercastle/authforge/core/model/UserHmacSecret.java b/core/src/main/java/org/clevercastle/authforge/core/model/UserHmacSecret.java index 963d0e1..f4a37d1 100644 --- a/core/src/main/java/org/clevercastle/authforge/core/model/UserHmacSecret.java +++ b/core/src/main/java/org/clevercastle/authforge/core/model/UserHmacSecret.java @@ -1,25 +1,9 @@ package org.clevercastle.authforge.core.model; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.IdClass; -import jakarta.persistence.Table; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserHmacSecretId; - import java.time.OffsetDateTime; -@javax.persistence.Entity -@javax.persistence.Table(name = "user_hmac_secret") -@Entity -@Table(name = "user_hmac_secret") -@javax.persistence.IdClass(RdsJpaUserHmacSecretId.class) -@IdClass(RdsJpaUserHmacSecretId.class) public class UserHmacSecret { - @javax.persistence.Id - @Id private String userId; - @javax.persistence.Id - @Id private String id; private String secret; diff --git a/core/src/main/java/org/clevercastle/authforge/core/model/UserLoginItem.java b/core/src/main/java/org/clevercastle/authforge/core/model/UserLoginItem.java index ab5a39f..6d47f04 100644 --- a/core/src/main/java/org/clevercastle/authforge/core/model/UserLoginItem.java +++ b/core/src/main/java/org/clevercastle/authforge/core/model/UserLoginItem.java @@ -1,11 +1,5 @@ package org.clevercastle.authforge.core.model; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.Id; -import jakarta.persistence.Table; - import java.time.OffsetDateTime; /** @@ -16,10 +10,6 @@ * * when the user register, it should automatically create a login item & create the corresponding user */ -@javax.persistence.Entity -@javax.persistence.Table(name = "user_login_item") -@Entity -@Table(name = "user_login_item") public class UserLoginItem { public enum Type { raw, @@ -32,17 +22,11 @@ public enum State { ACTIVE, } - @javax.persistence.Id - @Id private String loginIdentifier; private String loginIdentifierPrefix; - @javax.persistence.Enumerated(javax.persistence.EnumType.STRING) - @Enumerated(EnumType.STRING) private Type type; private String userSub; private String userId; - @javax.persistence.Enumerated(javax.persistence.EnumType.STRING) - @Enumerated(EnumType.STRING) private State state; // used for login item verification (loginType == raw) diff --git a/core/src/main/java/org/clevercastle/authforge/core/model/UserRefreshTokenMapping.java b/core/src/main/java/org/clevercastle/authforge/core/model/UserRefreshTokenMapping.java index 143f44c..deb9636 100644 --- a/core/src/main/java/org/clevercastle/authforge/core/model/UserRefreshTokenMapping.java +++ b/core/src/main/java/org/clevercastle/authforge/core/model/UserRefreshTokenMapping.java @@ -1,25 +1,9 @@ package org.clevercastle.authforge.core.model; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.IdClass; -import jakarta.persistence.Table; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserRefreshTokenMappingId; - import java.time.OffsetDateTime; -@javax.persistence.Entity -@javax.persistence.Table(name = "user_refresh_token") -@Entity -@Table(name = "user_refresh_token") -@javax.persistence.IdClass(RdsJpaUserRefreshTokenMappingId.class) -@IdClass(RdsJpaUserRefreshTokenMappingId.class) public class UserRefreshTokenMapping { - @javax.persistence.Id - @Id private String userId; - @javax.persistence.Id - @Id private String refreshToken; private OffsetDateTime expiredAt; diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/ChallengeSessionRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/ChallengeSessionRepository.java new file mode 100644 index 0000000..9f9847b --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/ChallengeSessionRepository.java @@ -0,0 +1,14 @@ +package org.clevercastle.authforge.core.repository; + +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.ChallengeSession; + +public interface ChallengeSessionRepository { + void createChallenge(ChallengeSession session) throws CastleException; + + ChallengeSession getById(String id) throws CastleException; + + void markVerified(String id) throws CastleException; + + void delete(String id) throws CastleException; +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/OneTimePasswordRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/OneTimePasswordRepository.java new file mode 100644 index 0000000..5077915 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/OneTimePasswordRepository.java @@ -0,0 +1,11 @@ +package org.clevercastle.authforge.core.repository; + +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.OneTimePassword; + +/** Repository for one-time passwords (OTP) per login identifier. */ +public interface OneTimePasswordRepository { + void saveOneTimePassword(OneTimePassword userOneTimePasswordMapping) throws CastleException; + + boolean verifyOneTimePassword(String loginIdentifier, String oneTimePassword) throws CastleException; +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/RefreshTokenRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..215e2ff --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/RefreshTokenRepository.java @@ -0,0 +1,21 @@ +package org.clevercastle.authforge.core.repository; + +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserRefreshTokenMapping; + +import java.time.OffsetDateTime; + +/** + * Repository for managing refresh tokens per user/session. + * + * Notes on semantics: + * - addRefreshToken: persist a new refresh token record with expiry. + * - verifyRefreshToken: verify (and typically consume/delete) an existing token. + * Implementations should ensure single-use by removing/invalidating upon success. + */ +public interface RefreshTokenRepository { + UserRefreshTokenMapping addRefreshToken(User user, String refreshToken, OffsetDateTime expiredAt) throws CastleException; + + boolean verifyRefreshToken(User user, String refreshToken) throws CastleException; +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/UserHmacSecretRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/UserHmacSecretRepository.java new file mode 100644 index 0000000..fc52ec4 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/UserHmacSecretRepository.java @@ -0,0 +1,16 @@ +package org.clevercastle.authforge.core.repository; + +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.UserHmacSecret; + +import java.util.List; + +public interface UserHmacSecretRepository { + void createHmacSecret(UserHmacSecret userHmacSecret) throws CastleException; + + List listHmacSecretByUserId(String userId) throws CastleException; + + void deleteHmacSecret(String userId, String id) throws CastleException; + + void touchLastUsedAt(String userId, String id) throws CastleException; +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/UserLoginItemRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/UserLoginItemRepository.java new file mode 100644 index 0000000..ba65209 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/UserLoginItemRepository.java @@ -0,0 +1,15 @@ +package org.clevercastle.authforge.core.repository; + +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.UserLoginItem; + +/** Repository for UserLoginItem (login identifiers). */ +public interface UserLoginItemRepository { + void confirmLoginItem(String loginIdentifier) throws CastleException; + + UserLoginItem save(UserLoginItem item) throws CastleException; + + UserLoginItem getByLoginIdentifier(String loginIdentifier) throws CastleException; + + UserLoginItem getByUserSub(String userSub) throws CastleException; +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/UserRegistrationRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/UserRegistrationRepository.java new file mode 100644 index 0000000..d0b561c --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/UserRegistrationRepository.java @@ -0,0 +1,11 @@ +package org.clevercastle.authforge.core.repository; + +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserLoginItem; + +/** Cross-aggregate write used by registration flows. */ +public interface UserRegistrationRepository { + void save(User user, UserLoginItem userLoginItem) throws CastleException; +} + diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/UserRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/UserRepository.java index 705a87c..0bf5f00 100644 --- a/core/src/main/java/org/clevercastle/authforge/core/repository/UserRepository.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/UserRepository.java @@ -1,40 +1,12 @@ package org.clevercastle.authforge.core.repository; -import jakarta.annotation.Nonnull; -import org.apache.commons.lang3.tuple.Pair; import org.clevercastle.authforge.core.exception.CastleException; -import org.clevercastle.authforge.core.model.ChallengeSession; -import org.clevercastle.authforge.core.model.OneTimePassword; import org.clevercastle.authforge.core.model.User; -import org.clevercastle.authforge.core.model.UserHmacSecret; -import org.clevercastle.authforge.core.model.UserLoginItem; -import org.clevercastle.authforge.core.model.UserRefreshTokenMapping; - -import java.time.OffsetDateTime; -import java.util.List; +/** Repository for User aggregate (user entity only). */ public interface UserRepository { - void save(User user, UserLoginItem userLoginItem) throws CastleException; - - @Nonnull - Pair getByLoginIdentifier(String loginIdentifier) throws CastleException; - - @Nonnull - Pair getByUserSub(String userSUb) throws CastleException; - - void confirmLoginItem(String loginIdentifier) throws CastleException; - - UserRefreshTokenMapping addRefreshToken(User user, String refreshToken, OffsetDateTime expiredAt) throws CastleException; - - boolean verifyRefreshToken(User user, String refreshToken) throws CastleException; - - void saveOneTimePassword(OneTimePassword userOneTimePasswordMapping) throws CastleException; - - boolean verifyOneTimePassword(String loginIdentifier, String oneTimePassword) throws CastleException; - - void createHmacSecret(UserHmacSecret userHmacSecret) throws CastleException; + User save(User user) throws CastleException; - List listHmacSecretByUserId(String userId) throws CastleException; + User getByUserId(String userId) throws CastleException; +} - void createChallenge(ChallengeSession session) throws CastleException; -} \ No newline at end of file diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUser.java b/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUser.java deleted file mode 100644 index d4b0fb1..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUser.java +++ /dev/null @@ -1,194 +0,0 @@ -package org.clevercastle.authforge.core.repository.dynamodb; - -import org.clevercastle.authforge.core.UserState; -import org.clevercastle.authforge.core.model.UserLoginItem; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; - -import java.time.OffsetDateTime; - -// single table design -@DynamoDbBean -public class DynamodbUser { - public enum Type { - user, - loginItem, - refreshToken - } - - public static final String TABLE_NAME = "auth-forge"; - - public static final String UserLoginItem_UserSub_index = "UserLoginItem_UserSub_index"; - - - // for user: pk = userId, sk = "user" - // for user login item: pk = loginIdentifier, sk = "loginItem" - // for user refresh token: pk = userId, sk="refreshToken#"+ refreshToken - private String pk; - private String sk; - - private Type type; - - // region from user table - private UserState userState; - private String userHashedPassword; - private String userResetPasswordCode; - private OffsetDateTime userResetPasswordCodeExpiredAt; - // endregion - - // region from user login item table - private String userLoginItemLoginIdentifierPrefix; - private UserLoginItem.Type userLoginItemType; - private String userLoginItemUserSub; - private String userLoginItemUserId; - private UserLoginItem.State userLoginItemState; - private String userLoginItemVerificationCode; - private OffsetDateTime userLoginItemVerificationCodeExpiredAt; - // endregion - - private OffsetDateTime userRefreshTokenExpiredAt; - - private OffsetDateTime createdAt; - private OffsetDateTime updatedAt; - - @DynamoDbPartitionKey - public String getPk() { - return pk; - } - - public void setPk(String pk) { - this.pk = pk; - } - - @DynamoDbSortKey - public String getSk() { - return sk; - } - - public void setSk(String sk) { - this.sk = sk; - } - - public Type getType() { - return type; - } - - public void setType(Type type) { - this.type = type; - } - - public UserState getUserState() { - return userState; - } - - public void setUserState(UserState userState) { - this.userState = userState; - } - - public String getUserHashedPassword() { - return userHashedPassword; - } - - public void setUserHashedPassword(String userHashedPassword) { - this.userHashedPassword = userHashedPassword; - } - - public String getUserResetPasswordCode() { - return userResetPasswordCode; - } - - public void setUserResetPasswordCode(String userResetPasswordCode) { - this.userResetPasswordCode = userResetPasswordCode; - } - - public OffsetDateTime getUserResetPasswordCodeExpiredAt() { - return userResetPasswordCodeExpiredAt; - } - - public void setUserResetPasswordCodeExpiredAt(OffsetDateTime userResetPasswordCodeExpiredAt) { - this.userResetPasswordCodeExpiredAt = userResetPasswordCodeExpiredAt; - } - - public String getUserLoginItemLoginIdentifierPrefix() { - return userLoginItemLoginIdentifierPrefix; - } - - public void setUserLoginItemLoginIdentifierPrefix(String userLoginItemLoginIdentifierPrefix) { - this.userLoginItemLoginIdentifierPrefix = userLoginItemLoginIdentifierPrefix; - } - - public UserLoginItem.Type getUserLoginItemType() { - return userLoginItemType; - } - - public void setUserLoginItemType(UserLoginItem.Type userLoginItemType) { - this.userLoginItemType = userLoginItemType; - } - - @DynamoDbSecondaryPartitionKey(indexNames = UserLoginItem_UserSub_index) - public String getUserLoginItemUserSub() { - return userLoginItemUserSub; - } - - public void setUserLoginItemUserSub(String userLoginItemUserSub) { - this.userLoginItemUserSub = userLoginItemUserSub; - } - - public String getUserLoginItemUserId() { - return userLoginItemUserId; - } - - public void setUserLoginItemUserId(String userLoginItemUserId) { - this.userLoginItemUserId = userLoginItemUserId; - } - - public UserLoginItem.State getUserLoginItemState() { - return userLoginItemState; - } - - public void setUserLoginItemState(UserLoginItem.State userLoginItemState) { - this.userLoginItemState = userLoginItemState; - } - - public String getUserLoginItemVerificationCode() { - return userLoginItemVerificationCode; - } - - public void setUserLoginItemVerificationCode(String userLoginItemVerificationCode) { - this.userLoginItemVerificationCode = userLoginItemVerificationCode; - } - - public OffsetDateTime getUserLoginItemVerificationCodeExpiredAt() { - return userLoginItemVerificationCodeExpiredAt; - } - - public void setUserLoginItemVerificationCodeExpiredAt(OffsetDateTime userLoginItemVerificationCodeExpiredAt) { - this.userLoginItemVerificationCodeExpiredAt = userLoginItemVerificationCodeExpiredAt; - } - - public OffsetDateTime getUserRefreshTokenExpiredAt() { - return userRefreshTokenExpiredAt; - } - - public void setUserRefreshTokenExpiredAt(OffsetDateTime userRefreshTokenExpiredAt) { - this.userRefreshTokenExpiredAt = userRefreshTokenExpiredAt; - } - - public OffsetDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(OffsetDateTime createdAt) { - this.createdAt = createdAt; - } - - public OffsetDateTime getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(OffsetDateTime updatedAt) { - this.updatedAt = updatedAt; - } -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUserRepositoryImpl.java b/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUserRepositoryImpl.java deleted file mode 100644 index c597a39..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUserRepositoryImpl.java +++ /dev/null @@ -1,195 +0,0 @@ -package org.clevercastle.authforge.core.repository.dynamodb; - -import jakarta.annotation.Nonnull; -import org.apache.commons.lang3.NotImplementedException; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.clevercastle.authforge.core.exception.CastleException; -import org.clevercastle.authforge.core.model.ChallengeSession; -import org.clevercastle.authforge.core.model.OneTimePassword; -import org.clevercastle.authforge.core.model.User; -import org.clevercastle.authforge.core.model.UserHmacSecret; -import org.clevercastle.authforge.core.model.UserLoginItem; -import org.clevercastle.authforge.core.model.UserRefreshTokenMapping; -import org.clevercastle.authforge.core.repository.UserRepository; -import org.clevercastle.authforge.core.util.TimeUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; -import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; -import software.amazon.awssdk.enhanced.dynamodb.Key; -import software.amazon.awssdk.enhanced.dynamodb.TableSchema; -import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; -import software.amazon.awssdk.services.dynamodb.model.DynamoDbException; -import software.amazon.awssdk.services.dynamodb.model.Put; -import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem; -import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest; -import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; - -import java.time.OffsetDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class DynamodbUserRepositoryImpl implements UserRepository { - private static final Logger logger = LoggerFactory.getLogger(DynamodbUserRepositoryImpl.class); - private final DynamoDbEnhancedClient enhancedClient; - private final DynamoDbClient lowClient; - - - private volatile DynamoDbTable table; - private volatile DynamoDbTable userLoginItemTable; - - public DynamodbUserRepositoryImpl(DynamoDbEnhancedClient enhancedClient, DynamoDbClient lowClient) { - this.enhancedClient = enhancedClient; - this.lowClient = lowClient; - } - - - @Override - public void save(User user, UserLoginItem userLoginItem) throws CastleException { - TransactWriteItem userWrite = TransactWriteItem.builder() - .put(Put.builder() - .tableName(DynamodbUser.TABLE_NAME) - .item(getTable().tableSchema().itemToMap(DynamodbUserUtil.fromUser(user), true)) - .build()) - .build(); - TransactWriteItem userLoginItemWrite = TransactWriteItem.builder() - .put(Put.builder() - .tableName(DynamodbUser.TABLE_NAME) - .item(getTable().tableSchema().itemToMap(DynamodbUserUtil.fromUserLogItem(userLoginItem), true)) - .build()) - .build(); - - TransactWriteItemsRequest transactWriteItemsRequest = TransactWriteItemsRequest.builder() - .transactItems(Arrays.asList(userWrite, userLoginItemWrite)) - .build(); - try { - lowClient.transactWriteItems(transactWriteItemsRequest); - } catch (DynamoDbException e) { - logger.error("Fail to save user", e); - throw new CastleException(); - } - } - - @Nonnull - @Override - public Pair getByLoginIdentifier(String loginIdentifier) { - DynamodbUser dynamodbUser = getTable().getItem(Key.builder().partitionValue(loginIdentifier).sortValue(DynamodbUser.Type.loginItem.name()).build()); - if (dynamodbUser == null) { - return Pair.of(null, null); - } - UserLoginItem userLoginItem = DynamodbUserUtil.toUserLogItem(dynamodbUser); - dynamodbUser = getTable().getItem(Key.builder().partitionValue(userLoginItem.getUserId()).sortValue(DynamodbUser.Type.user.name()).build()); - if (dynamodbUser == null) { - logger.error(""); - return Pair.of(null, null); - } - return Pair.of(DynamodbUserUtil.toUser(dynamodbUser), userLoginItem); - } - - @Nonnull - @Override - public Pair getByUserSub(String userSub) { - List dynamodbUsers = getTable().index(DynamodbUser.UserLoginItem_UserSub_index) - .query(QueryConditional.keyEqualTo(Key.builder().partitionValue(userSub).build())) - .stream().flatMap(it -> it.items().stream()).collect(Collectors.toList()); - if (dynamodbUsers.isEmpty()) { - return Pair.of(null, null); - } - if (dynamodbUsers.size() > 1) { - logger.error("More than one user found for user sub {}", userSub); - - } - DynamodbUser dynamodbUser = dynamodbUsers.get(0); - UserLoginItem userLoginItem = DynamodbUserUtil.toUserLogItem(dynamodbUser); - dynamodbUser = getTable().getItem(Key.builder().partitionValue(userLoginItem.getUserId()).sortValue(DynamodbUser.Type.user.name()).build()); - if (dynamodbUser == null) { - logger.error(""); - return Pair.of(null, null); - } - return Pair.of(DynamodbUserUtil.toUser(dynamodbUser), userLoginItem); - } - - @Override - public void confirmLoginItem(String loginIdentifier) { - UpdateItemRequest updateItemRequest = UpdateItemRequest.builder() - .tableName(DynamodbUser.TABLE_NAME) - .key(Map.of("pk", AttributeValue.fromS(loginIdentifier), "sk", AttributeValue.fromS(DynamodbUser.Type.loginItem.name()))) - .updateExpression("Set userLoginItemState = :state Remove userLoginItemVerificationCode, userLoginItemVerificationCodeExpiredAt") - .expressionAttributeValues(Map.of(":state", AttributeValue.fromS(UserLoginItem.State.ACTIVE.name()))) - .build(); - this.lowClient.updateItem(updateItemRequest); - } - - @Override - public UserRefreshTokenMapping addRefreshToken(User user, String refreshToken, OffsetDateTime expiredAt) { - UserRefreshTokenMapping mapping = new UserRefreshTokenMapping(); - mapping.setUserId(user.getUserId()); - mapping.setRefreshToken(DynamodbUser.Type.refreshToken + "#" + refreshToken); - OffsetDateTime now = OffsetDateTime.now(); - mapping.setCreatedAt(now); - mapping.setExpiredAt(expiredAt); - getTable().putItem(DynamodbUserUtil.fromUserRefreshTokenMapping(mapping)); - return mapping; - } - - @Override - public boolean verifyRefreshToken(User user, String refreshToken) throws CastleException { - if (StringUtils.isBlank(refreshToken)) { - return false; - } - Key key = Key.builder().partitionValue(user.getUserId()).sortValue(DynamodbUser.Type.refreshToken + "#" + refreshToken).build(); - DynamodbUser dynamodbUser = getTable() - .getItem(key); - if (dynamodbUser == null) { - return false; - } - UserRefreshTokenMapping userRefreshTokenMapping = DynamodbUserUtil.toUserRefreshTokenMapping(dynamodbUser); - if (userRefreshTokenMapping.getExpiredAt() != null && userRefreshTokenMapping.getExpiredAt().isAfter(TimeUtils.now())) { - getTable().deleteItem(key); - return true; - } - return false; - } - - private DynamoDbTable getTable() { - if (table == null) { - synchronized (this) { - if (table == null) { - TableSchema schema = TableSchema.fromBean(DynamodbUser.class); - table = enhancedClient.table(DynamodbUser.TABLE_NAME, schema); - } - } - } - return table; - } - - @Override - public void saveOneTimePassword(OneTimePassword userOneTimePasswordMapping) throws CastleException { - throw new NotImplementedException(); - } - - @Override - public boolean verifyOneTimePassword(String loginIdentifier, String oneTimePassword) throws CastleException { - throw new NotImplementedException(); - } - - @Override - public void createHmacSecret(UserHmacSecret userHmacSecret) throws CastleException { - - } - - @Override - public List listHmacSecretByUserId(String userId) throws CastleException { - return List.of(); - } - - @Override - public void createChallenge(ChallengeSession session) throws CastleException { - - } -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUserUtil.java b/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUserUtil.java deleted file mode 100644 index 2a598e8..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUserUtil.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.clevercastle.authforge.core.repository.dynamodb; - -import org.clevercastle.authforge.core.model.User; -import org.clevercastle.authforge.core.model.UserLoginItem; -import org.clevercastle.authforge.core.model.UserRefreshTokenMapping; - -public class DynamodbUserUtil { - - public static DynamodbUser fromUser(User user) { - DynamodbUser dynamodbUser = new DynamodbUser(); - dynamodbUser.setPk(user.getUserId()); - dynamodbUser.setSk(DynamodbUser.Type.user.name()); - - dynamodbUser.setUserState(user.getUserState()); - dynamodbUser.setUserHashedPassword(user.getHashedPassword()); - dynamodbUser.setUserResetPasswordCode(user.getResetPasswordCode()); - dynamodbUser.setUserResetPasswordCodeExpiredAt(user.getResetPasswordCodeExpiredAt()); - - dynamodbUser.setCreatedAt(user.getCreatedAt()); - dynamodbUser.setUpdatedAt(user.getUpdatedAt()); - return dynamodbUser; - } - - public static DynamodbUser fromUserLogItem(UserLoginItem userLoginItem) { - DynamodbUser dynamodbUser = new DynamodbUser(); - dynamodbUser.setPk(userLoginItem.getLoginIdentifier()); - dynamodbUser.setSk(DynamodbUser.Type.loginItem.name()); - - dynamodbUser.setUserLoginItemLoginIdentifierPrefix(userLoginItem.getLoginIdentifierPrefix()); - dynamodbUser.setUserLoginItemType(userLoginItem.getType()); - dynamodbUser.setUserLoginItemUserSub(userLoginItem.getUserSub()); - dynamodbUser.setUserLoginItemUserId(userLoginItem.getUserId()); - dynamodbUser.setUserLoginItemState(userLoginItem.getState()); - dynamodbUser.setUserLoginItemVerificationCode(userLoginItem.getVerificationCode()); - dynamodbUser.setUserLoginItemVerificationCodeExpiredAt(userLoginItem.getVerificationCodeExpiredAt()); - - dynamodbUser.setCreatedAt(userLoginItem.getCreatedAt()); - dynamodbUser.setUpdatedAt(userLoginItem.getUpdatedAt()); - return dynamodbUser; - } - - public static DynamodbUser fromUserRefreshTokenMapping(UserRefreshTokenMapping userRefreshTokenMapping) { - DynamodbUser dynamodbUser = new DynamodbUser(); - dynamodbUser.setPk(userRefreshTokenMapping.getUserId()); - dynamodbUser.setSk(userRefreshTokenMapping.getRefreshToken()); - dynamodbUser.setUserRefreshTokenExpiredAt(userRefreshTokenMapping.getExpiredAt()); - - dynamodbUser.setCreatedAt(userRefreshTokenMapping.getCreatedAt()); - return dynamodbUser; - } - - public static User toUser(DynamodbUser dynamodbUser) { - User user = new User(); - user.setUserId(dynamodbUser.getPk()); - user.setUserState(dynamodbUser.getUserState()); - user.setHashedPassword(dynamodbUser.getUserHashedPassword()); - user.setResetPasswordCode(dynamodbUser.getUserResetPasswordCode()); - user.setResetPasswordCodeExpiredAt(dynamodbUser.getUserResetPasswordCodeExpiredAt()); - user.setCreatedAt(dynamodbUser.getCreatedAt()); - user.setUpdatedAt(dynamodbUser.getUpdatedAt()); - return user; - } - - public static UserLoginItem toUserLogItem(DynamodbUser dynamodbUser) { - UserLoginItem userLoginItem = new UserLoginItem(); - userLoginItem.setLoginIdentifier(dynamodbUser.getPk()); - userLoginItem.setLoginIdentifierPrefix(dynamodbUser.getUserLoginItemLoginIdentifierPrefix()); - userLoginItem.setType(dynamodbUser.getUserLoginItemType()); - userLoginItem.setUserSub(dynamodbUser.getUserLoginItemUserSub()); - userLoginItem.setUserId(dynamodbUser.getUserLoginItemUserId()); - userLoginItem.setState(dynamodbUser.getUserLoginItemState()); - userLoginItem.setVerificationCode(dynamodbUser.getUserLoginItemVerificationCode()); - userLoginItem.setVerificationCodeExpiredAt(dynamodbUser.getUserLoginItemVerificationCodeExpiredAt()); - userLoginItem.setCreatedAt(dynamodbUser.getCreatedAt()); - userLoginItem.setUpdatedAt(dynamodbUser.getUpdatedAt()); - return userLoginItem; - } - - public static UserRefreshTokenMapping toUserRefreshTokenMapping(DynamodbUser dynamodbUser) { - UserRefreshTokenMapping userRefreshTokenMapping = new UserRefreshTokenMapping(); - userRefreshTokenMapping.setUserId(dynamodbUser.getPk()); - userRefreshTokenMapping.setRefreshToken(dynamodbUser.getSk()); - userRefreshTokenMapping.setExpiredAt(dynamodbUser.getUserRefreshTokenExpiredAt()); - - userRefreshTokenMapping.setCreatedAt(dynamodbUser.getCreatedAt()); - return userRefreshTokenMapping; - } -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaChallengeSessionRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaChallengeSessionRepository.java deleted file mode 100644 index b4d2bde..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaChallengeSessionRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.clevercastle.authforge.core.repository.rdsjpa; - -import org.clevercastle.authforge.core.model.ChallengeSession; - -public interface RdsJpaChallengeSessionRepository { - ChallengeSession save(ChallengeSession challengeSession); - - ChallengeSession getById(String id); -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaOneTimePasswordId.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaOneTimePasswordId.java deleted file mode 100644 index e343f99..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaOneTimePasswordId.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.clevercastle.authforge.core.repository.rdsjpa; - -import java.io.Serializable; - -public class RdsJpaOneTimePasswordId implements Serializable { - private String loginIdentifier; - private String oneTimePassword; - - public RdsJpaOneTimePasswordId() { - } - - public RdsJpaOneTimePasswordId(String loginIdentifier, String oneTimePassword) { - this.loginIdentifier = loginIdentifier; - this.oneTimePassword = oneTimePassword; - } - - public String getLoginIdentifier() { - return loginIdentifier; - } - - public void setLoginIdentifier(String loginIdentifier) { - this.loginIdentifier = loginIdentifier; - } - - public String getOneTimePassword() { - return oneTimePassword; - } - - public void setOneTimePassword(String oneTimePassword) { - this.oneTimePassword = oneTimePassword; - } -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaOneTimePasswordRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaOneTimePasswordRepository.java deleted file mode 100644 index f330e03..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaOneTimePasswordRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.clevercastle.authforge.core.repository.rdsjpa; - -import org.clevercastle.authforge.core.model.OneTimePassword; - -import java.util.List; - -public interface RdsJpaOneTimePasswordRepository { - OneTimePassword save(OneTimePassword oneTimePassword); - - void deleteByLoginIdentifier(String loginIdentifier); - - List getByLoginIdentifier(String loginIdentifier); -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserHmacSecretId.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserHmacSecretId.java deleted file mode 100644 index d9f9fc2..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserHmacSecretId.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.clevercastle.authforge.core.repository.rdsjpa; - -import java.io.Serializable; - -public class RdsJpaUserHmacSecretId implements Serializable { - private String userId; - private String id; - - public String getUserId() { - return userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserHmacSecretRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserHmacSecretRepository.java deleted file mode 100644 index 61ec4b6..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserHmacSecretRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.clevercastle.authforge.core.repository.rdsjpa; - -import org.clevercastle.authforge.core.model.UserHmacSecret; - -import java.util.List; - -public interface RdsJpaUserHmacSecretRepository { - UserHmacSecret save(UserHmacSecret userHmacSecret); - - List getByUserId(String userId); -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserLoginItemRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserLoginItemRepository.java deleted file mode 100644 index b9724d7..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserLoginItemRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.clevercastle.authforge.core.repository.rdsjpa; - -import org.clevercastle.authforge.core.model.UserLoginItem; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; - -public interface RdsJpaUserLoginItemRepository { - UserLoginItem save(UserLoginItem userLoginItem); - - UserLoginItem getByLoginIdentifier(String loginIdentifier); - - UserLoginItem getByUserSub(String userSub); - - @Modifying - @Query("UPDATE UserLoginItem u\n" + - " SET u.state = 'ACTIVE', \n" + - " u.verificationCode = NULL\n" + - " WHERE u.loginIdentifier = :loginIdentifier") - void confirmLoginItem(String loginIdentifier); -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserModelRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserModelRepository.java deleted file mode 100644 index ceb8212..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserModelRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.clevercastle.authforge.core.repository.rdsjpa; - -import org.clevercastle.authforge.core.model.User; - -public interface RdsJpaUserModelRepository { - User save(User user); - - User getByUserId(String userId); -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRefreshTokenMappingId.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRefreshTokenMappingId.java deleted file mode 100644 index 48679bf..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRefreshTokenMappingId.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.clevercastle.authforge.core.repository.rdsjpa; - -import java.io.Serializable; - -public class RdsJpaUserRefreshTokenMappingId implements Serializable { - private String userId; - private String refreshToken; - - public RdsJpaUserRefreshTokenMappingId() { - } - - public RdsJpaUserRefreshTokenMappingId(String userId, String refreshToken) { - this.userId = userId; - this.refreshToken = refreshToken; - } - - public String getUserId() { - return userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } - - public String getRefreshToken() { - return refreshToken; - } - - public void setRefreshToken(String refreshToken) { - this.refreshToken = refreshToken; - } -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRefreshTokenMappingRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRefreshTokenMappingRepository.java deleted file mode 100644 index 2008811..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRefreshTokenMappingRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.clevercastle.authforge.core.repository.rdsjpa; - -import org.clevercastle.authforge.core.model.UserRefreshTokenMapping; - -public interface RdsJpaUserRefreshTokenMappingRepository { - UserRefreshTokenMapping getByUserIdAndRefreshToken(String userIed, String refreshToken); - - void deleteByUserIdAndRefreshToken(String userIed, String refreshToken); - - UserRefreshTokenMapping save(UserRefreshTokenMapping userRefreshTokenMapping); -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRepositoryImpl.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRepositoryImpl.java deleted file mode 100644 index 690c558..0000000 --- a/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRepositoryImpl.java +++ /dev/null @@ -1,133 +0,0 @@ -package org.clevercastle.authforge.core.repository.rdsjpa; - -import jakarta.annotation.Nonnull; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.clevercastle.authforge.core.exception.CastleException; -import org.clevercastle.authforge.core.model.ChallengeSession; -import org.clevercastle.authforge.core.model.OneTimePassword; -import org.clevercastle.authforge.core.model.User; -import org.clevercastle.authforge.core.model.UserHmacSecret; -import org.clevercastle.authforge.core.model.UserLoginItem; -import org.clevercastle.authforge.core.model.UserRefreshTokenMapping; -import org.clevercastle.authforge.core.repository.UserRepository; -import org.clevercastle.authforge.core.util.TimeUtils; - -import java.time.OffsetDateTime; -import java.util.List; - -public class RdsJpaUserRepositoryImpl implements UserRepository { - private final RdsJpaUserModelRepository userModelRepository; - private final RdsJpaUserLoginItemRepository userLoginItemRepository; - private final RdsJpaUserRefreshTokenMappingRepository userRefreshTokenMappingRepository; - private final RdsJpaOneTimePasswordRepository oneTimePasswordRepository; - private final RdsJpaChallengeSessionRepository challengeSessionRepository; - private final RdsJpaUserHmacSecretRepository userHmacSecretRepository; - - public RdsJpaUserRepositoryImpl(RdsJpaUserModelRepository userModelRepository, - RdsJpaUserLoginItemRepository userLoginItemRepository, - RdsJpaUserRefreshTokenMappingRepository userRefreshTokenMappingRepository, - RdsJpaOneTimePasswordRepository oneTimePasswordRepository, - RdsJpaChallengeSessionRepository challengeSessionRepository, - RdsJpaUserHmacSecretRepository userHmacSecretRepository) { - this.userModelRepository = userModelRepository; - this.userLoginItemRepository = userLoginItemRepository; - this.userRefreshTokenMappingRepository = userRefreshTokenMappingRepository; - this.oneTimePasswordRepository = oneTimePasswordRepository; - this.challengeSessionRepository = challengeSessionRepository; - this.userHmacSecretRepository = userHmacSecretRepository; - } - - @Override - public void save(User user, UserLoginItem userLoginItem) { - userModelRepository.save(user); - userLoginItemRepository.save(userLoginItem); - } - - @Nonnull - @Override - public Pair getByLoginIdentifier(String loginIdentifier) { - UserLoginItem userLoginItem = userLoginItemRepository.getByLoginIdentifier(loginIdentifier); - if (userLoginItem != null) { - return Pair.of(userModelRepository.getByUserId(userLoginItem.getUserId()), userLoginItem); - } - return Pair.of(null, null); - } - - @Override - public void confirmLoginItem(String loginIdentifier) { - userLoginItemRepository.confirmLoginItem(loginIdentifier); - } - - @Override - public UserRefreshTokenMapping addRefreshToken(User user, String refreshToken, OffsetDateTime expiredAt) { - UserRefreshTokenMapping mapping = new UserRefreshTokenMapping(); - mapping.setUserId(user.getUserId()); - mapping.setRefreshToken(refreshToken); - OffsetDateTime now = OffsetDateTime.now(); - mapping.setCreatedAt(now); - mapping.setExpiredAt(expiredAt); - userRefreshTokenMappingRepository.save(mapping); - return mapping; - } - - @Override - public boolean verifyRefreshToken(@Nonnull User user, String refreshToken) throws CastleException { - if (StringUtils.isBlank(refreshToken)) { - return false; - } - UserRefreshTokenMapping mapping = userRefreshTokenMappingRepository.getByUserIdAndRefreshToken(user.getUserId(), refreshToken); - if (mapping == null) { - return false; - } - if (mapping.getExpiredAt() != null && mapping.getExpiredAt().isAfter(TimeUtils.now())) { - userRefreshTokenMappingRepository.deleteByUserIdAndRefreshToken(user.getUserId(), refreshToken); - return true; - } - return false; - } - - @Nonnull - @Override - public Pair getByUserSub(String userSub) { - UserLoginItem userLoginItem = userLoginItemRepository.getByUserSub(userSub); - if (userLoginItem != null) { - return Pair.of(userModelRepository.getByUserId(userLoginItem.getUserId()), userLoginItem); - } - return Pair.of(null, null); - } - - @Override - public void saveOneTimePassword(OneTimePassword oneTimePassword) throws CastleException { - oneTimePasswordRepository.deleteByLoginIdentifier(oneTimePassword.getLoginIdentifier()); - oneTimePasswordRepository.save(oneTimePassword); - } - - @Override - public boolean verifyOneTimePassword(String loginIdentifier, String oneTimePassword) throws CastleException { - // the result's size should be one - List oneTimePasswords = oneTimePasswordRepository.getByLoginIdentifier(loginIdentifier); - for (OneTimePassword otp : oneTimePasswords) { - if (StringUtils.equals(otp.getOneTimePassword(), oneTimePassword)) { - oneTimePasswordRepository.deleteByLoginIdentifier(loginIdentifier); - return true; - } - } - return false; - } - - @Override - public void createHmacSecret(UserHmacSecret userHmacSecret) throws CastleException { - this.userHmacSecretRepository.save(userHmacSecret); - } - - @Override - public List listHmacSecretByUserId(String userId) throws CastleException { - return this.userHmacSecretRepository.getByUserId(userId); - } - - @Override - public void createChallenge(ChallengeSession session) throws CastleException { - this.challengeSessionRepository.save(session); - } -} diff --git a/core/src/main/java/org/clevercastle/authforge/core/service/MfaService.java b/core/src/main/java/org/clevercastle/authforge/core/service/MfaService.java new file mode 100644 index 0000000..72a88b6 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/service/MfaService.java @@ -0,0 +1,30 @@ +package org.clevercastle.authforge.core.service; + +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.mfa.dto.MfaChallengeResponse; +import org.clevercastle.authforge.core.mfa.dto.MfaFactorResponse; +import org.clevercastle.authforge.core.model.ChallengeSession; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.totp.RequestTotpResponse; +import org.clevercastle.authforge.core.totp.SetupTotpRequest; + +import java.util.List; + +public interface MfaService { + ChallengeSession createChallenge(String userId, ChallengeSession.Type type); + + RequestTotpResponse requestTotp(User user) throws CastleException; + + void setupTotp(User user, SetupTotpRequest request) throws CastleException; + + MfaChallengeResponse createMfaChallenge(User user, String challengeType, String factorId) throws CastleException; + + boolean verifyMfaChallenge(String challengeId, String code, String bindingCode) throws CastleException; + + List listMfaFactors(String userId) throws CastleException; + + void deleteMfaFactor(String userId, String factorId) throws CastleException; + + boolean verifyTotpCode(String userId, String code) throws CastleException; +} + diff --git a/core/src/main/java/org/clevercastle/authforge/core/service/OtpService.java b/core/src/main/java/org/clevercastle/authforge/core/service/OtpService.java new file mode 100644 index 0000000..3c8a22a --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/service/OtpService.java @@ -0,0 +1,12 @@ +package org.clevercastle.authforge.core.service; + +import org.clevercastle.authforge.core.UserWithToken; +import org.clevercastle.authforge.core.dto.OneTimePasswordDto; +import org.clevercastle.authforge.core.exception.CastleException; + +public interface OtpService { + OneTimePasswordDto requestOneTimePassword(String loginIdentifier) throws CastleException; + + UserWithToken verifyOneTimePassword(String loginIdentifier, String oneTimePassword) throws CastleException; +} + diff --git a/core/src/main/java/org/clevercastle/authforge/core/service/TokenSessionService.java b/core/src/main/java/org/clevercastle/authforge/core/service/TokenSessionService.java new file mode 100644 index 0000000..2fbaa33 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/service/TokenSessionService.java @@ -0,0 +1,11 @@ +package org.clevercastle.authforge.core.service; + +import org.clevercastle.authforge.core.UserWithToken; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserLoginItem; + +public interface TokenSessionService { + UserWithToken refresh(User user, UserLoginItem userLoginItem, String refreshToken) throws CastleException; +} + diff --git a/core/src/main/java/org/clevercastle/authforge/core/service/UserAuthService.java b/core/src/main/java/org/clevercastle/authforge/core/service/UserAuthService.java new file mode 100644 index 0000000..2b0d07a --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/service/UserAuthService.java @@ -0,0 +1,26 @@ +package org.clevercastle.authforge.core.service; + +import org.apache.commons.lang3.tuple.Pair; +import org.clevercastle.authforge.core.UserRegisterRequest; +import org.clevercastle.authforge.core.UserWithToken; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.oauth2.Oauth2ClientConfig; + +public interface UserAuthService { + User register(UserRegisterRequest request) throws CastleException; + + void verify(String loginIdentifier, String verificationCode) throws CastleException; + + UserWithToken login(String loginIdentifier, String password) throws CastleException; + + Pair getByLoginIdentifier(String loginIdentifier) throws CastleException; + + Pair getByUserSub(String userSub) throws CastleException; + + String generate(Oauth2ClientConfig oauth2Client, String redirectUri); + + UserWithToken exchange(Oauth2ClientConfig clientConfig, String authorizationCode, String state, String redirectUrl) throws CastleException; +} + diff --git a/core/src/main/java/org/clevercastle/authforge/core/service/impl/MfaServiceImpl.java b/core/src/main/java/org/clevercastle/authforge/core/service/impl/MfaServiceImpl.java new file mode 100644 index 0000000..d197195 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/service/impl/MfaServiceImpl.java @@ -0,0 +1,186 @@ +package org.clevercastle.authforge.core.service.impl; + +import org.apache.commons.lang3.StringUtils; +import org.clevercastle.authforge.core.CacheService; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.mfa.dto.MfaChallengeResponse; +import org.clevercastle.authforge.core.mfa.dto.MfaFactorResponse; +import org.clevercastle.authforge.core.model.ChallengeSession; +import org.clevercastle.authforge.core.model.ResourceType; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserHmacSecret; +import org.clevercastle.authforge.core.repository.ChallengeSessionRepository; +import org.clevercastle.authforge.core.repository.UserHmacSecretRepository; +import org.clevercastle.authforge.core.service.MfaService; +import org.clevercastle.authforge.core.totp.RequestTotpResponse; +import org.clevercastle.authforge.core.totp.SetupTotpRequest; +import org.clevercastle.authforge.core.totp.TotpUtil; +import org.clevercastle.authforge.core.util.IdUtil; +import org.clevercastle.authforge.core.util.TimeUtils; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +public class MfaServiceImpl implements MfaService { + private final CacheService cacheService; + private final UserHmacSecretRepository userHmacSecretRepository; + private final ChallengeSessionRepository challengeSessionRepository; + + public MfaServiceImpl(CacheService cacheService, + UserHmacSecretRepository userHmacSecretRepository, + ChallengeSessionRepository challengeSessionRepository) { + this.cacheService = cacheService; + this.userHmacSecretRepository = userHmacSecretRepository; + this.challengeSessionRepository = challengeSessionRepository; + } + + @Override + public ChallengeSession createChallenge(String userId, ChallengeSession.Type type) { + ChallengeSession session = new ChallengeSession(); + session.setId(IdUtil.genId(ResourceType.challengeSession)); + session.setType(type); + session.setUserId(userId); + session.setCreatedAt(TimeUtils.now()); + return session; + } + + @Override + public RequestTotpResponse requestTotp(User user) throws CastleException { + String sessionId = UUID.randomUUID().toString(); + String secret = TotpUtil.generateSecret(); + cacheService.set(sessionId, secret, 300); + + String accountName = user.getUserId(); + String issuerName = "AuthForge"; + String qrCodeUri = TotpUtil.generateQRCodeUri(secret, accountName, issuerName); + + RequestTotpResponse response = new RequestTotpResponse(); + response.setSessionId(sessionId); + response.setSecret(secret); + response.setQrCodeUri(qrCodeUri); + response.setManualEntryKey(secret); + return response; + } + + @Override + public void setupTotp(User user, SetupTotpRequest request) throws CastleException { + String secret = cacheService.get(request.getSessionId()); + if (StringUtils.isBlank(secret)) { + throw new CastleException("Invalid session ID or session expired"); + } + if (request.getCodes() == null || request.getCodes().isEmpty()) { + throw new CastleException("Verification codes are required"); + } + boolean isVerified = false; + for (var codeEntry : request.getCodes()) { + if (codeEntry.getCode() != null && codeEntry.getInputTime() != null) { + long timeSeconds = codeEntry.getInputTime().toEpochSecond(); + if (TotpUtil.verifyTOTPAtTime(codeEntry.getCode(), secret, timeSeconds)) { + isVerified = true; + break; + } + } + } + if (!isVerified) { + throw new CastleException("Invalid verification code"); + } + List existingSecrets = userHmacSecretRepository.listHmacSecretByUserId(user.getUserId()); + if (!existingSecrets.isEmpty()) { + throw new CastleException("TOTP already configured for this user"); + } + UserHmacSecret userHmacSecret = new UserHmacSecret(); + userHmacSecret.setUserId(user.getUserId()); + userHmacSecret.setId(IdUtil.genId(ResourceType.totp)); + userHmacSecret.setSecret(secret); + var now = TimeUtils.now(); + userHmacSecret.setCreatedAt(now); + userHmacSecret.setLastUsedAt(now); + userHmacSecret.setName(StringUtils.isNotBlank(request.getName()) ? request.getName() : "TOTP Device"); + userHmacSecretRepository.createHmacSecret(userHmacSecret); + cacheService.delete(request.getSessionId()); + } + + @Override + public MfaChallengeResponse createMfaChallenge(User user, String challengeType, String factorId) throws CastleException { + if (!"totp".equals(challengeType)) { + throw new CastleException("Unsupported challenge type: " + challengeType); + } + List userSecrets = userHmacSecretRepository.listHmacSecretByUserId(user.getUserId()); + UserHmacSecret targetSecret = null; + for (UserHmacSecret secret : userSecrets) { + if (secret.getId().equals(factorId)) { + targetSecret = secret; + break; + } + } + if (targetSecret == null) { + throw new CastleException("MFA factor not found"); + } + ChallengeSession challengeSession = createChallenge(user.getUserId(), ChallengeSession.Type.mfa); + challengeSession.setCreatedAt(TimeUtils.now()); + challengeSession.setUserId(user.getUserId()); + challengeSessionRepository.createChallenge(challengeSession); + cacheService.set("mfa_challenge_" + challengeSession.getId(), factorId, 300); + + MfaChallengeResponse response = new MfaChallengeResponse(); + response.setChallengeId(challengeSession.getId()); + response.setChallengeType(challengeType); + response.setUserId(user.getUserId()); + response.setExpiresAt(TimeUtils.now().plusSeconds(300)); + return response; + } + + @Override + public boolean verifyMfaChallenge(String challengeId, String code, String bindingCode) throws CastleException { + String factorId = cacheService.get("mfa_challenge_" + challengeId); + if (StringUtils.isBlank(factorId)) { + throw new CastleException("Challenge session not found or expired"); + } + try { + // TODO: load secret by factorId if needed + return TotpUtil.verifyTOTP(code, "dummy_secret"); + } catch (Exception e) { + return false; + } finally { + cacheService.delete("mfa_challenge_" + challengeId); + } + } + + @Override + public List listMfaFactors(String userId) throws CastleException { + List secrets = userHmacSecretRepository.listHmacSecretByUserId(userId); + return secrets.stream().map(secret -> { + MfaFactorResponse factor = new MfaFactorResponse(); + factor.setId(secret.getId()); + factor.setType("totp"); + factor.setName(secret.getName()); + factor.setActive(true); + factor.setCreatedAt(secret.getCreatedAt()); + factor.setLastUsedAt(secret.getLastUsedAt()); + return factor; + }).collect(Collectors.toList()); + } + + @Override + public void deleteMfaFactor(String userId, String factorId) throws CastleException { + userHmacSecretRepository.deleteHmacSecret(userId, factorId); + } + + @Override + public boolean verifyTotpCode(String userId, String code) throws CastleException { + List secrets = userHmacSecretRepository.listHmacSecretByUserId(userId); + for (UserHmacSecret secret : secrets) { + if (TotpUtil.verifyTOTP(code, secret.getSecret())) { + secret.setLastUsedAt(TimeUtils.now()); + try { + userHmacSecretRepository.touchLastUsedAt(userId, secret.getId()); + } catch (Exception ignored) { + } + return true; + } + } + return false; + } +} + diff --git a/core/src/main/java/org/clevercastle/authforge/core/service/impl/OtpServiceImpl.java b/core/src/main/java/org/clevercastle/authforge/core/service/impl/OtpServiceImpl.java new file mode 100644 index 0000000..550b348 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/service/impl/OtpServiceImpl.java @@ -0,0 +1,89 @@ +package org.clevercastle.authforge.core.service.impl; + +import org.apache.commons.lang3.tuple.Pair; +import org.clevercastle.authforge.core.Config; +import org.clevercastle.authforge.core.UserState; +import org.clevercastle.authforge.core.UserWithToken; +import org.clevercastle.authforge.core.code.CodeSender; +import org.clevercastle.authforge.core.dto.OneTimePasswordDto; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.exception.UserNotFoundException; +import org.clevercastle.authforge.core.model.OneTimePassword; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.repository.OneTimePasswordRepository; +import org.clevercastle.authforge.core.repository.RefreshTokenRepository; +import org.clevercastle.authforge.core.service.OtpService; +import org.clevercastle.authforge.core.service.UserAuthService; +import org.clevercastle.authforge.core.token.TokenService; +import org.clevercastle.authforge.core.util.CodeUtil; +import org.clevercastle.authforge.core.util.TimeUtils; + +public class OtpServiceImpl implements OtpService { + private final Config config; + private final OneTimePasswordRepository oneTimePasswordRepository; + private final TokenService tokenService; + private final CodeSender codeSender; + private final RefreshTokenRepository refreshTokenRepository; + private final UserAuthService userAuthService; + + public OtpServiceImpl(Config config, + OneTimePasswordRepository oneTimePasswordRepository, + TokenService tokenService, + CodeSender codeSender, + RefreshTokenRepository refreshTokenRepository, + UserAuthService userAuthService) { + this.config = config; + this.oneTimePasswordRepository = oneTimePasswordRepository; + this.tokenService = tokenService; + this.codeSender = codeSender; + this.refreshTokenRepository = refreshTokenRepository; + this.userAuthService = userAuthService; + } + + @Override + public OneTimePasswordDto requestOneTimePassword(String loginIdentifier) throws CastleException { + Pair pair = userAuthService.getByLoginIdentifier(loginIdentifier); + if (pair.getLeft() == null || pair.getRight() == null) { + throw new UserNotFoundException(); + } + if (UserLoginItem.State.ACTIVE != pair.getRight().getState()) { + throw new CastleException("Current login is not confirmed"); + } + if (UserState.ACTIVE != pair.getLeft().getUserState()) { + throw new CastleException("The user is not confirmed"); + } + OneTimePassword oneTimePassword = new OneTimePassword(); + oneTimePassword.setLoginIdentifier(loginIdentifier); + oneTimePassword.setOneTimePassword(CodeUtil.generateCode(6, CodeUtil.UPPER_CHARS)); + oneTimePassword.setExpiredAt(TimeUtils.now().plusSeconds(config.getOneTimePasswordExpireTime())); + oneTimePassword.setCreatedAt(TimeUtils.now()); + oneTimePasswordRepository.saveOneTimePassword(oneTimePassword); + this.codeSender.sendOneTimePassword(loginIdentifier, oneTimePassword.getOneTimePassword()); + OneTimePasswordDto oneTimePasswordDto = new OneTimePasswordDto(); + oneTimePasswordDto.setLoginIdentifier(loginIdentifier); + oneTimePasswordDto.setExpiredAt(oneTimePassword.getExpiredAt()); + oneTimePasswordDto.setCreatedAt(oneTimePassword.getCreatedAt()); + return oneTimePasswordDto; + } + + @Override + public UserWithToken verifyOneTimePassword(String loginIdentifier, String oneTimePassword) throws CastleException { + if (!oneTimePasswordRepository.verifyOneTimePassword(loginIdentifier, oneTimePassword)) { + throw new CastleException(); + } + Pair pair = userAuthService.getByLoginIdentifier(loginIdentifier); + if (pair.getLeft() == null || pair.getRight() == null) { + throw new UserNotFoundException(); + } + if (UserLoginItem.State.ACTIVE != pair.getRight().getState()) { + throw new CastleException("Current login is not confirmed"); + } + if (UserState.ACTIVE != pair.getLeft().getUserState()) { + throw new CastleException("The user is not confirmed"); + } + var tokenHolder = tokenService.generateToken(pair.getLeft(), pair.getRight()); + refreshTokenRepository.addRefreshToken(pair.getLeft(), tokenHolder.getRefreshToken(), tokenHolder.getExpiresAt()); + return new UserWithToken(pair.getLeft(), tokenHolder); + } +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/service/impl/TokenSessionServiceImpl.java b/core/src/main/java/org/clevercastle/authforge/core/service/impl/TokenSessionServiceImpl.java new file mode 100644 index 0000000..7ab940f --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/service/impl/TokenSessionServiceImpl.java @@ -0,0 +1,34 @@ +package org.clevercastle.authforge.core.service.impl; + +import jakarta.transaction.Transactional; +import org.clevercastle.authforge.core.UserWithToken; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.repository.RefreshTokenRepository; +import org.clevercastle.authforge.core.service.TokenSessionService; +import org.clevercastle.authforge.core.token.TokenService; + +public class TokenSessionServiceImpl implements TokenSessionService { + private final RefreshTokenRepository refreshTokenRepository; + private final TokenService tokenService; + + public TokenSessionServiceImpl(RefreshTokenRepository refreshTokenRepository, TokenService tokenService) { + this.refreshTokenRepository = refreshTokenRepository; + this.tokenService = tokenService; + } + + @Override + @javax.transaction.Transactional(value = javax.transaction.Transactional.TxType.REQUIRED) + @Transactional(value = Transactional.TxType.REQUIRED) + public UserWithToken refresh(User user, UserLoginItem userLoginItem, String refreshToken) throws CastleException { + boolean verified = refreshTokenRepository.verifyRefreshToken(user, refreshToken); + if (!verified) { + throw new CastleException("Fail to refresh"); + } + var tokenHolder = tokenService.generateToken(user, userLoginItem); + refreshTokenRepository.addRefreshToken(user, tokenHolder.getRefreshToken(), tokenHolder.getExpiresAt()); + return new UserWithToken(user, tokenHolder); + } +} + diff --git a/core/src/main/java/org/clevercastle/authforge/core/service/impl/UserAuthServiceImpl.java b/core/src/main/java/org/clevercastle/authforge/core/service/impl/UserAuthServiceImpl.java new file mode 100644 index 0000000..6721486 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/service/impl/UserAuthServiceImpl.java @@ -0,0 +1,236 @@ +package org.clevercastle.authforge.core.service.impl; + +import jakarta.annotation.Nonnull; +import jakarta.transaction.Transactional; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.clevercastle.authforge.core.CacheService; +import org.clevercastle.authforge.core.Config; +import org.clevercastle.authforge.core.UserRegisterRequest; +import org.clevercastle.authforge.core.UserState; +import org.clevercastle.authforge.core.UserWithToken; +import org.clevercastle.authforge.core.code.CodeSender; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.exception.UserExistException; +import org.clevercastle.authforge.core.exception.UserNotFoundException; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.oauth2.Oauth2ClientConfig; +import org.clevercastle.authforge.core.oauth2.Oauth2User; +import org.clevercastle.authforge.core.repository.RefreshTokenRepository; +import org.clevercastle.authforge.core.repository.UserLoginItemRepository; +import org.clevercastle.authforge.core.repository.UserRepository; +import org.clevercastle.authforge.core.service.UserAuthService; +import org.clevercastle.authforge.core.token.TokenService; +import org.clevercastle.authforge.core.util.CodeUtil; +import org.clevercastle.authforge.core.util.HashUtil; +import org.clevercastle.authforge.core.util.IdUtil; +import org.clevercastle.authforge.core.util.TimeUtils; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +public class UserAuthServiceImpl implements UserAuthService { + private final Config config; + private final UserRepository userModelRepository; + private final UserLoginItemRepository loginItemRepository; + private final RefreshTokenRepository refreshTokenRepository; + private final TokenService tokenService; + private final CodeSender codeSender; + private final CacheService cacheService; + + public UserAuthServiceImpl(Config config, + UserRepository userModelRepository, + UserLoginItemRepository loginItemRepository, + RefreshTokenRepository refreshTokenRepository, + TokenService tokenService, + CodeSender codeSender, + CacheService cacheService) { + this.config = config; + this.userModelRepository = userModelRepository; + this.loginItemRepository = loginItemRepository; + this.refreshTokenRepository = refreshTokenRepository; + this.tokenService = tokenService; + this.codeSender = codeSender; + this.cacheService = cacheService; + } + + @Override + @Transactional + public User register(UserRegisterRequest userRegisterRequest) throws CastleException { + Pair pair = this.getByLoginIdentifier(userRegisterRequest.getLoginIdentifier()); + User user = pair.getLeft(); + if (user != null) { + if (UserState.DELETED != user.getUserState()) { + throw new UserExistException(); + } + } + String userId = IdUtil.genUserId(); + var now = TimeUtils.now(); + user = new User(); + user.setUserId(userId); + user.setUserState(UserState.ACTIVE); + user.setHashedPassword(HashUtil.hashPassword(userRegisterRequest.getPassword())); + user.setCreatedAt(now); + user.setUpdatedAt(now); + + UserLoginItem userLoginItem = new UserLoginItem(); + userLoginItem.setUserId(userId); + userLoginItem.setUserSub(UUID.randomUUID().toString()); + userLoginItem.setType(UserLoginItem.Type.raw); + userLoginItem.setLoginIdentifier(userRegisterRequest.getLoginIdentifier()); + userLoginItem.setLoginIdentifierPrefix(userRegisterRequest.getLoginIdentifierPrefix()); + userLoginItem.setState(UserLoginItem.State.UNCONFIRMED); + userLoginItem.setVerificationCode(CodeUtil.generateCode(8)); + userLoginItem.setVerificationCodeExpiredAt(TimeUtils.now().plusSeconds(this.config.getVerificationCodeExpireTime())); + userLoginItem.setCreatedAt(now); + userLoginItem.setUpdatedAt(now); + userModelRepository.save(user); + loginItemRepository.save(userLoginItem); + this.codeSender.sendVerificationCode(userLoginItem.getLoginIdentifier(), userLoginItem.getVerificationCode()); + return user; + } + + @Override + @javax.transaction.Transactional + @Transactional + public void verify(String loginIdentifier, String verificationCode) throws CastleException { + Pair pair = this.getByLoginIdentifier(loginIdentifier); + // if not found, return + if (pair.getLeft() == null || pair.getRight() == null) { + throw new UserNotFoundException(); + } + var userLoginItem = pair.getRight(); + if (UserLoginItem.State.ACTIVE == userLoginItem.getState()) { + throw new CastleException(); + } + if (userLoginItem.getVerificationCodeExpiredAt() == null || userLoginItem.getVerificationCodeExpiredAt().isBefore(TimeUtils.now())) { + throw new CastleException(); + } + if (StringUtils.equals(verificationCode, userLoginItem.getVerificationCode())) { + loginItemRepository.confirmLoginItem(loginIdentifier); + } else { + throw new CastleException(); + } + } + + @Override + @Transactional + public UserWithToken login(String loginIdentifier, String password) throws CastleException { + Pair pair = getByLoginIdentifier(loginIdentifier); + var user = pair.getLeft(); + var userLoginItem = pair.getRight(); + if (user == null) { + throw new UserNotFoundException(); + } + if (UserLoginItem.State.ACTIVE != userLoginItem.getState()) { + throw new CastleException("Current login is not confirmed"); + } + if (UserState.ACTIVE != user.getUserState()) { + throw new CastleException(""); + } + boolean verify = HashUtil.verifyPassword(password, user.getHashedPassword()); + if (!verify) { + throw new CastleException("Incorrect password"); + } + var tokenHolder = tokenService.generateToken(user, userLoginItem); + refreshTokenRepository.addRefreshToken(user, tokenHolder.getRefreshToken(), tokenHolder.getExpiresAt()); + return new UserWithToken(user, tokenHolder); + } + + @Nonnull + @Override + public Pair getByLoginIdentifier(String loginIdentifier) throws CastleException { + try { + // Get login item first + UserLoginItem loginItem = loginItemRepository.getByLoginIdentifier(loginIdentifier); + if (loginItem == null) { + return Pair.of(null, null); + } + // Get user by userId from login item + User user = userModelRepository.getByUserId(loginItem.getUserId()); + + return Pair.of(user, loginItem); + } catch (CastleException e) { + throw e; + } catch (Exception e) { + throw new CastleException("Failed to get user identity by login identifier: " + e.getMessage(), e); + } + } + + @Override + public Pair getByUserSub(String userSub) throws CastleException { + try { + // Get login item first + UserLoginItem loginItem = loginItemRepository.getByUserSub(userSub); + if (loginItem == null) { + return Pair.of(null, null); + } + // Get user by userId from login item + User user = userModelRepository.getByUserId(loginItem.getUserId()); + return Pair.of(user, loginItem); + } catch (CastleException e) { + throw e; + } catch (Exception e) { + throw new CastleException("Failed to get user identity by userSub: " + e.getMessage(), e); + } + } + + @Override + public String generate(Oauth2ClientConfig oauth2Client, String redirectUrl) { + Map map = new LinkedHashMap<>(); + if (oauth2Client.getMandatoryQueryParams() != null) { + map.putAll(oauth2Client.getMandatoryQueryParams()); + } + map.put("client_id", oauth2Client.getClientId()); + map.put("redirect_uri", redirectUrl); + map.put("response_type", "code"); + map.put("scope", StringUtils.join(oauth2Client.getScopes(), "%20")); + map.put("state", UUID.randomUUID().toString()); + String queryString = map.entrySet().stream().map(it -> String.format("%s=%s", it.getKey(), it.getValue())).collect(java.util.stream.Collectors.joining("&")); + return oauth2Client.getOauth2LoginUrl() + "?" + queryString; + } + + @Override + public UserWithToken exchange(Oauth2ClientConfig clientConfig, String authorizationCode, String state, String redirectUrl) throws CastleException { + Oauth2User oauth2User = clientConfig.getOauth2ExchangeService().exchange(clientConfig, authorizationCode, state, redirectUrl); + if (StringUtils.isBlank(oauth2User.getLoginIdentifier())) { + throw new CastleException(); + } + Pair pair = getByLoginIdentifier(oauth2User.getLoginIdentifier()); + var user = pair.getLeft(); + var userLoginItem = pair.getRight(); + if (userLoginItem == null) { + // register process + String userId = IdUtil.genUserId(); + var now = TimeUtils.now(); + user = new User(); + user.setUserId(userId); + user.setUserState(UserState.ACTIVE); + user.setCreatedAt(now); + user.setUpdatedAt(now); + + userLoginItem = new UserLoginItem(); + userLoginItem.setUserId(userId); + userLoginItem.setLoginIdentifier(oauth2User.getLoginIdentifier()); + userLoginItem.setUserSub(UUID.randomUUID().toString()); + userLoginItem.setCreatedAt(now); + userLoginItem.setUpdatedAt(now); + userModelRepository.save(user); + loginItemRepository.save(userLoginItem); + var tokenHolder = tokenService.generateToken(user, userLoginItem); + refreshTokenRepository.addRefreshToken(user, tokenHolder.getRefreshToken(), tokenHolder.getExpiresAt()); + return new UserWithToken(user, tokenHolder); + } else { + // login process + if (user == null || UserState.ACTIVE != user.getUserState()) { + throw new CastleException(""); + } + var tokenHolder = tokenService.generateToken(user, userLoginItem); + refreshTokenRepository.addRefreshToken(user, tokenHolder.getRefreshToken(), tokenHolder.getExpiresAt()); + return new UserWithToken(user, tokenHolder); + } + } +} + diff --git a/examples/spring-boot-example/build.gradle.kts b/examples/spring-boot-example/build.gradle.kts index d8c64e7..e2444f3 100644 --- a/examples/spring-boot-example/build.gradle.kts +++ b/examples/spring-boot-example/build.gradle.kts @@ -1,34 +1,39 @@ +import org.springframework.boot.gradle.plugin.SpringBootPlugin + plugins { java - id("org.springframework.boot") version "2.7.18" + id("org.springframework.boot") version "3.5.6" id("io.spring.dependency-management") version "1.1.7" } group = "org.clevercastle" version = "0.1.0-SNAPSHOT" - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(11) +dependencyManagement { + imports { + mavenBom(SpringBootPlugin.BOM_COORDINATES) + //Testing dependencies + mavenBom("org.junit:junit-bom:5.13.4") + mavenBom("software.amazon.awssdk:bom:2.34.4") + mavenBom("org.testcontainers:testcontainers-bom:1.21.3") + mavenBom("com.fasterxml.jackson:jackson-bom:2.19.2") } } dependencies { developmentOnly("org.springframework.boot:spring-boot-devtools") implementation(project(":core")) + implementation(project(":impls:impl-postgres")) - implementation("org.apache.httpcomponents.client5:httpclient5:5.4.2") - implementation("org.apache.httpcomponents.core5:httpcore5:5.3.4") - - implementation("software.amazon.awssdk:dynamodb:2.31.31") - implementation("software.amazon.awssdk:dynamodb-enhanced:2.31.31") + implementation("org.apache.httpcomponents.client5:httpclient5:5.4.4") + implementation("org.apache.httpcomponents.core5:httpcore5:5.3.6") - implementation("org.apache.commons:commons-lang3:3.18.0") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-data-jpa") + runtimeOnly("org.postgresql:postgresql:42.7.7") - implementation("com.auth0:auth0:2.24.0") + implementation("com.auth0:java-jwt:4.5.0") + testImplementation("org.springframework.boot:spring-boot-starter-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/examples/spring-boot-example/gradle/wrapper/gradle-wrapper.properties b/examples/spring-boot-example/gradle/wrapper/gradle-wrapper.properties index 37f853b..ca025c8 100644 --- a/examples/spring-boot-example/gradle/wrapper/gradle-wrapper.properties +++ b/examples/spring-boot-example/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/AuthController.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/AuthController.java index 2a2cceb..022f03d 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/AuthController.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/AuthController.java @@ -5,7 +5,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.clevercastle.authforge.core.UserRegisterRequest; -import org.clevercastle.authforge.core.UserService; import org.clevercastle.authforge.core.UserWithToken; import org.clevercastle.authforge.core.dto.OneTimePasswordDto; import org.clevercastle.authforge.core.exception.CastleException; @@ -14,6 +13,9 @@ import org.clevercastle.authforge.core.oauth2.Oauth2ClientConfig; import org.clevercastle.authforge.core.oauth2.github.GithubOauth2ExchangeService; import org.clevercastle.authforge.core.oauth2.oidc.OidcExchangeService; +import org.clevercastle.authforge.core.service.OtpService; +import org.clevercastle.authforge.core.service.TokenSessionService; +import org.clevercastle.authforge.core.service.UserAuthService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -32,8 +34,8 @@ public class AuthController { .oauth2ExchangeService(new OidcExchangeService()) .clientId("") .clientSecret("") - .oauth2LoginUrl("https://accounts.google.com/o/oauth2/v2/auth") - .oauth2TokenUrl("https://oauth2.googleapis.com/token") + .oauth2LoginUrl("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") + .oauth2TokenUrl("https://login.microsoftonline.com/common/oauth2/v2.0/token") .scopes(List.of("openid", "profile", "email")) .emailFunction((map) -> { Object email = map.get("email"); @@ -70,10 +72,14 @@ public class AuthController { .build(); - private final UserService userService; + private final UserAuthService userAuthService; + private final OtpService otpService; + private final TokenSessionService tokenSessionService; - public AuthController(UserService userService) { - this.userService = userService; + public AuthController(UserAuthService userAuthService, OtpService otpService, TokenSessionService tokenSessionService) { + this.userAuthService = userAuthService; + this.otpService = otpService; + this.tokenSessionService = tokenSessionService; } @PostMapping("auth/register") @@ -82,13 +88,13 @@ public User register(@RequestBody RegisterRequest request) throws CastleExceptio userRegisterRequest.setLoginIdentifier("email#" + request.getEmail()); userRegisterRequest.setPassword(request.getPassword()); userRegisterRequest.setLoginIdentifierPrefix("email"); - return userService.register(userRegisterRequest); + return userAuthService.register(userRegisterRequest); } @GetMapping("auth/verify") public UserWithToken verify(@RequestParam String email, @RequestParam String verificationCode) throws CastleException { - userService.verify("email#" + email, verificationCode); + userAuthService.verify("email#" + email, verificationCode); return null; } @@ -100,7 +106,7 @@ public UserWithToken login(@RequestHeader String authorization) throws CastleExc String[] credentials = new String(Base64.getDecoder().decode(authorization)).split(":"); String loginIdentifier = "email#" + credentials[0]; String password = credentials[1]; - return userService.login(loginIdentifier, password); + return userAuthService.login(loginIdentifier, password); } @GetMapping("auth/refresh") @@ -113,11 +119,11 @@ public UserWithToken login(@RequestHeader String authorization, @RequestBody Ref if (StringUtils.isBlank(userSub)) { throw new CastleException(""); } - Pair pair = userService.getByUserSub(userSub); + Pair pair = userAuthService.getByUserSub(userSub); if (pair.getLeft() == null || pair.getRight() == null) { throw new CastleException(""); } - return userService.refresh(pair.getLeft(), pair.getRight(), refreshToken.getRefreshToken()); + return tokenSessionService.refresh(pair.getLeft(), pair.getRight(), refreshToken.getRefreshToken()); } @GetMapping("auth/sso/url") @@ -125,9 +131,9 @@ public String generateUrl(@RequestParam SsoType ssoType, @RequestParam String re // decode basic authentication switch (ssoType) { case google: - return userService.generate(googleClientConfig, redirectUrl); + return userAuthService.generate(googleClientConfig, redirectUrl); case github: - return userService.generate(githubClientConfig, redirectUrl); + return userAuthService.generate(githubClientConfig, redirectUrl); } return null; } @@ -136,20 +142,20 @@ public String generateUrl(@RequestParam SsoType ssoType, @RequestParam String re public UserWithToken exchange(@RequestParam SsoType ssoType, @RequestParam String code, @RequestParam String state, @RequestParam String redirectUrl) throws CastleException { switch (ssoType) { case google: - return userService.exchange(googleClientConfig, code, state, redirectUrl); + return userAuthService.exchange(googleClientConfig, code, state, redirectUrl); case github: - return userService.exchange(githubClientConfig, code, state, redirectUrl); + return userAuthService.exchange(githubClientConfig, code, state, redirectUrl); } return null; } @GetMapping("auth/one-time-password") public OneTimePasswordDto requestOneTimePassword(@RequestParam String email) throws CastleException { - return userService.requestOneTimePassword("email#" + email); + return otpService.requestOneTimePassword("email#" + email); } @PostMapping("auth/one-time-password") public UserWithToken verifyOneTimePassword(@RequestBody VerifyOneTimeRequest request) throws CastleException { - return userService.verifyOneTimePassword("email#" + request.getEmail(), request.getOneTimePassword()); + return otpService.verifyOneTimePassword("email#" + request.getEmail(), request.getOneTimePassword()); } } diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/Beans.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/Beans.java index 3d544a3..6f87ed6 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/Beans.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/Beans.java @@ -1,28 +1,27 @@ package org.clevercastle.authforge.core.examples.springboot.springbootexample; import com.auth0.jwt.algorithms.Algorithm; -import org.clevercastle.authforge.core.DummyCacheServiceImpl; +import org.clevercastle.authforge.core.CacheService; import org.clevercastle.authforge.core.Config; -import org.clevercastle.authforge.core.UserService; -import org.clevercastle.authforge.core.UserServiceImpl; +import org.clevercastle.authforge.core.DummyCacheServiceImpl; +import org.clevercastle.authforge.core.code.CodeSender; +import org.clevercastle.authforge.core.code.DummyCodeSender; +import org.clevercastle.authforge.core.repository.OneTimePasswordRepository; +import org.clevercastle.authforge.core.repository.RefreshTokenRepository; +import org.clevercastle.authforge.core.repository.UserLoginItemRepository; import org.clevercastle.authforge.core.repository.UserRepository; -import org.clevercastle.authforge.core.repository.dynamodb.DynamodbUserRepositoryImpl; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaChallengeSessionRepository; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaOneTimePasswordRepository; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserHmacSecretRepository; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserLoginItemRepository; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserModelRepository; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserRefreshTokenMappingRepository; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserRepositoryImpl; +import org.clevercastle.authforge.core.service.OtpService; +import org.clevercastle.authforge.core.service.TokenSessionService; +import org.clevercastle.authforge.core.service.UserAuthService; +import org.clevercastle.authforge.core.service.impl.OtpServiceImpl; +import org.clevercastle.authforge.core.service.impl.TokenSessionServiceImpl; +import org.clevercastle.authforge.core.service.impl.UserAuthServiceImpl; import org.clevercastle.authforge.core.token.TokenService; import org.clevercastle.authforge.core.token.jwt.JwtTokenService; -import org.clevercastle.authforge.core.code.DummyCodeSender; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; @@ -37,33 +36,9 @@ @Configuration public class Beans { - @Bean - public UserRepository userRepository(RdsJpaUserModelRepository userModelRepository, - RdsJpaUserLoginItemRepository userLoginItemRepository, - RdsJpaUserRefreshTokenMappingRepository userRefreshTokenMappingRepository, - RdsJpaOneTimePasswordRepository oneTimePasswordRepository, - RdsJpaChallengeSessionRepository challengeSessionRepository, - RdsJpaUserHmacSecretRepository userHmacSecretRepository) { - return new RdsJpaUserRepositoryImpl(userModelRepository, userLoginItemRepository, - userRefreshTokenMappingRepository, oneTimePasswordRepository, challengeSessionRepository, userHmacSecretRepository); - } - - - @Bean - public UserRepository dynamodbUserRepository(DynamoDbEnhancedClient dynamodbEnhancedClient, DynamoDbClient dynamodbClient) { - return new DynamodbUserRepositoryImpl(dynamodbEnhancedClient, dynamodbClient); - } - - @Bean - public DynamoDbClient dynamoDbClient() { - return DynamoDbClient.builder().build(); - } - - @Bean - public DynamoDbEnhancedClient dynamoDbEnhancedClient(DynamoDbClient dynamoDbClient) { - return DynamoDbEnhancedClient.builder().dynamoDbClient(dynamoDbClient).build(); - } + // PostgreSQL repository implementations are auto-detected by Spring Boot via @Repository annotations + // No need to manually create beans - Spring will auto-wire them @Bean public TokenService tokenService() throws NoSuchAlgorithmException, InvalidKeySpecException { @@ -84,10 +59,48 @@ public TokenService tokenService() throws NoSuchAlgorithmException, InvalidKeySp } @Bean - public UserService userService(UserRepository userRepository, TokenService tokenService) { - return new UserServiceImpl(Config.builder().build(), userRepository, tokenService, new DummyCodeSender(), new DummyCacheServiceImpl()); + public Config config() { + return Config.builder().build(); + } + + @Bean + public CodeSender codeSender() { + return new DummyCodeSender(); } + @Bean + public CacheService cacheService() { + return new DummyCacheServiceImpl(); + } + + @Bean + public UserAuthService userAuthService(Config config, + UserRepository userModelRepository, + UserLoginItemRepository loginItemRepository, + RefreshTokenRepository refreshTokenRepository, + TokenService tokenService, + CodeSender codeSender, + CacheService cacheService) { + return new UserAuthServiceImpl(config, userModelRepository, loginItemRepository, + refreshTokenRepository, tokenService, codeSender, cacheService); + } + + @Bean + public OtpService otpService(Config config, + OneTimePasswordRepository oneTimePasswordRepository, + TokenService tokenService, + CodeSender codeSender, + RefreshTokenRepository refreshTokenRepository, + UserAuthService userAuthService) { + return new OtpServiceImpl(config, oneTimePasswordRepository, tokenService, + codeSender, refreshTokenRepository, userAuthService); + } + + @Bean + public TokenSessionService tokenSessionService(RefreshTokenRepository refreshTokenRepository, + TokenService tokenService) { + return new TokenSessionServiceImpl(refreshTokenRepository, tokenService); + } @Bean public WebMvcConfigurer corsConfigurer() { @@ -98,4 +111,4 @@ public void addCorsMappings(CorsRegistry registry) { } }; } -} +} \ No newline at end of file diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/ChallengeSessionRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/ChallengeSessionRepositoryAdapter.java deleted file mode 100644 index 2984fd8..0000000 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/ChallengeSessionRepositoryAdapter.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.clevercastle.authforge.core.examples.springboot.springbootexample; - -import org.clevercastle.authforge.core.model.ChallengeSession; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaChallengeSessionRepository; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ChallengeSessionRepositoryAdapter extends RdsJpaChallengeSessionRepository, JpaRepository { -} diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/OneTimePasswordRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/OneTimePasswordRepositoryAdapter.java deleted file mode 100644 index b9448d5..0000000 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/OneTimePasswordRepositoryAdapter.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.clevercastle.authforge.core.examples.springboot.springbootexample; - -import org.clevercastle.authforge.core.model.OneTimePassword; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaOneTimePasswordId; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaOneTimePasswordRepository; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface OneTimePasswordRepositoryAdapter extends RdsJpaOneTimePasswordRepository, JpaRepository { -} diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/RepositoryBeans.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/RepositoryBeans.java new file mode 100644 index 0000000..0111f2f --- /dev/null +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/RepositoryBeans.java @@ -0,0 +1,66 @@ +package org.clevercastle.authforge.core.examples.springboot.springbootexample; + +import jakarta.persistence.EntityManager; +import org.clevercastle.authforge.core.repository.ChallengeSessionRepository; +import org.clevercastle.authforge.core.repository.OneTimePasswordRepository; +import org.clevercastle.authforge.core.repository.RefreshTokenRepository; +import org.clevercastle.authforge.core.repository.UserHmacSecretRepository; +import org.clevercastle.authforge.core.repository.UserLoginItemRepository; +import org.clevercastle.authforge.core.repository.UserRegistrationRepository; +import org.clevercastle.authforge.core.repository.UserRepository; +import org.clevercastle.authforge.impl.postgres.repository.PostgresChallengeSessionRepository; +import org.clevercastle.authforge.impl.postgres.repository.PostgresLoginItemRepository; +import org.clevercastle.authforge.impl.postgres.repository.PostgresOneTimePasswordRepository; +import org.clevercastle.authforge.impl.postgres.repository.PostgresRefreshTokenRepository; +import org.clevercastle.authforge.impl.postgres.repository.PostgresUserHmacSecretRepository; +import org.clevercastle.authforge.impl.postgres.repository.PostgresUserModelRepository; +import org.clevercastle.authforge.impl.postgres.repository.PostgresUserRegistrationRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; + +@Configuration +public class RepositoryBeans { + + @Bean + public UserRepository userModelRepository(EntityManager entityManager) { + JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager); + return new PostgresUserModelRepository(jpaRepositoryFactory); + } + + @Bean + public UserLoginItemRepository loginItemRepository(EntityManager entityManager) { + JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager); + return new PostgresLoginItemRepository(jpaRepositoryFactory); + } + + @Bean + public UserRegistrationRepository userRegistrationRepository(EntityManager entityManager) { + JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager); + return new PostgresUserRegistrationRepository(jpaRepositoryFactory); + } + + @Bean + public RefreshTokenRepository refreshTokenRepository(EntityManager entityManager) { + JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager); + return new PostgresRefreshTokenRepository(jpaRepositoryFactory); + } + + @Bean + public OneTimePasswordRepository oneTimePasswordRepository(EntityManager entityManager) { + JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager); + return new PostgresOneTimePasswordRepository(jpaRepositoryFactory); + } + + @Bean + public UserHmacSecretRepository userHmacSecretRepository(EntityManager entityManager) { + JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager); + return new PostgresUserHmacSecretRepository(jpaRepositoryFactory); + } + + @Bean + public ChallengeSessionRepository challengeSessionRepository(EntityManager entityManager) { + JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager); + return new PostgresChallengeSessionRepository(jpaRepositoryFactory); + } +} \ No newline at end of file diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/SpringBootExampleApplication.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/SpringBootExampleApplication.java index b8a8d08..d790c17 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/SpringBootExampleApplication.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/SpringBootExampleApplication.java @@ -1,12 +1,17 @@ package org.clevercastle.authforge.core.examples.springboot.springbootexample; -import org.clevercastle.authforge.core.model.User; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @SpringBootApplication -@EntityScan(basePackageClasses = User.class) +@EntityScan(basePackages = {"org.clevercastle.authforge.core.model", "org.clevercastle.authforge.impl.postgres.entity"}) +@EnableJpaRepositories(basePackages = {"org.clevercastle.authforge.impl.postgres.repository"}) +@ComponentScan(basePackages = {"org.clevercastle.authforge"}) +@Import({Beans.class, RepositoryBeans.class}) public class SpringBootExampleApplication { public static void main(String[] args) { diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserController.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserController.java deleted file mode 100644 index fdae3f2..0000000 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserController.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.clevercastle.authforge.core.examples.springboot.springbootexample; - -import org.clevercastle.authforge.core.UserService; -import org.clevercastle.authforge.core.exception.CastleException; -import org.clevercastle.authforge.core.model.User; -import org.clevercastle.authforge.core.totp.RequestTotpResponse; -import org.clevercastle.authforge.core.totp.SetupTotpRequest; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class UserController { - - private final UserService userService; - - public UserController(UserService userService) { - this.userService = userService; - } - - @PostMapping("user/mfa/request") - public RequestTotpResponse requestTotp() throws CastleException { - User user = new User(); - return userService.requestTotp(user); - } - - @PostMapping("user/mfa/verify") - public void verifyTotp(@RequestBody SetupTotpRequest request) throws CastleException { - User user = new User(); - user.setUserId("user-01"); - userService.setupTotp(user, request); - } -} diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserHmacSecretRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserHmacSecretRepositoryAdapter.java deleted file mode 100644 index 5fcebbf..0000000 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserHmacSecretRepositoryAdapter.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.clevercastle.authforge.core.examples.springboot.springbootexample; - -import org.clevercastle.authforge.core.model.UserHmacSecret; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserHmacSecretId; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserHmacSecretRepository; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface UserHmacSecretRepositoryAdapter extends RdsJpaUserHmacSecretRepository, JpaRepository { -} diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserLoginItemRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserLoginItemRepositoryAdapter.java deleted file mode 100644 index 84d6c37..0000000 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserLoginItemRepositoryAdapter.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.clevercastle.authforge.core.examples.springboot.springbootexample; - -import org.clevercastle.authforge.core.model.UserLoginItem; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserLoginItemRepository; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface UserLoginItemRepositoryAdapter extends RdsJpaUserLoginItemRepository, JpaRepository { -} \ No newline at end of file diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserModelRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserModelRepositoryAdapter.java deleted file mode 100644 index 939b8a8..0000000 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserModelRepositoryAdapter.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.clevercastle.authforge.core.examples.springboot.springbootexample; - -import org.clevercastle.authforge.core.model.User; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserModelRepository; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface UserModelRepositoryAdapter extends RdsJpaUserModelRepository, JpaRepository { -} \ No newline at end of file diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserRefreshTokenMappingRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserRefreshTokenMappingRepositoryAdapter.java deleted file mode 100644 index 656b984..0000000 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserRefreshTokenMappingRepositoryAdapter.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.clevercastle.authforge.core.examples.springboot.springbootexample; - -import org.clevercastle.authforge.core.model.UserRefreshTokenMapping; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserRefreshTokenMappingRepository; -import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserRefreshTokenMappingId; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface UserRefreshTokenMappingRepositoryAdapter extends RdsJpaUserRefreshTokenMappingRepository, JpaRepository { -} diff --git a/examples/spring-boot-example/src/main/resources/application.yaml b/examples/spring-boot-example/src/main/resources/application.yaml index b0ba699..12ad2e3 100644 --- a/examples/spring-boot-example/src/main/resources/application.yaml +++ b/examples/spring-boot-example/src/main/resources/application.yaml @@ -7,6 +7,8 @@ spring: jpa: hibernate: ddl-auto: update + naming: + physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy show-sql: true properties: hibernate: diff --git a/impls/impl-postgres/README.md b/impls/impl-postgres/README.md new file mode 100644 index 0000000..c69bff8 --- /dev/null +++ b/impls/impl-postgres/README.md @@ -0,0 +1 @@ +Spring Boot 3 JPA implementation for PostgreSQL \ No newline at end of file diff --git a/impls/impl-postgres/build.gradle.kts b/impls/impl-postgres/build.gradle.kts index f5914ae..d06a89f 100644 --- a/impls/impl-postgres/build.gradle.kts +++ b/impls/impl-postgres/build.gradle.kts @@ -1,3 +1,19 @@ dependencies { - runtimeOnly("org.postgresql:postgresql:42.7.7") + implementation(project(":core")) + + implementation("org.springframework:spring-context:6.2.11") + // Spring Data JPA + implementation("org.springframework.data:spring-data-commons:3.5.4") { + isTransitive = false + } + implementation("org.springframework.data:spring-data-jpa:3.5.4") { + isTransitive = false + } + + implementation("jakarta.persistence:jakarta.persistence-api:3.1.0") + implementation("jakarta.transaction:jakarta.transaction-api:2.0.1") + + // MapStruct dependencies + implementation("org.mapstruct:mapstruct:1.5.5.Final") + annotationProcessor("org.mapstruct:mapstruct-processor:1.5.5.Final") } \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/ChallengeSessionEntity.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/ChallengeSessionEntity.java new file mode 100644 index 0000000..5dc75c6 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/ChallengeSessionEntity.java @@ -0,0 +1,80 @@ +package org.clevercastle.authforge.impl.postgres.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import org.clevercastle.authforge.core.model.ChallengeSession; + +import java.time.OffsetDateTime; + +@Entity +@Table(name = "challenge_sessions") +public class ChallengeSessionEntity { + + @Id + @Column + private String id; + + @Enumerated(EnumType.STRING) + @Column + private ChallengeSession.Type type; + + @Column + private String userId; + + @Column + private boolean verified = false; + + @Column + private OffsetDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = OffsetDateTime.now(); + } + + // Getters and setters + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public ChallengeSession.Type getType() { + return type; + } + + public void setType(ChallengeSession.Type type) { + this.type = type; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public boolean isVerified() { + return verified; + } + + public void setVerified(boolean verified) { + this.verified = verified; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/OneTimePasswordEntity.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/OneTimePasswordEntity.java new file mode 100644 index 0000000..8422ca7 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/OneTimePasswordEntity.java @@ -0,0 +1,65 @@ +package org.clevercastle.authforge.impl.postgres.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; + +import java.time.OffsetDateTime; + +@Entity +@Table(name = "one_time_passwords") +public class OneTimePasswordEntity { + + @Id + @Column + private String loginIdentifier; + + @Column + private String oneTimePassword; + + @Column + private OffsetDateTime expiredAt; + + @Column + private OffsetDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = OffsetDateTime.now(); + } + + // Getters and setters + public String getLoginIdentifier() { + return loginIdentifier; + } + + public void setLoginIdentifier(String loginIdentifier) { + this.loginIdentifier = loginIdentifier; + } + + public String getOneTimePassword() { + return oneTimePassword; + } + + public void setOneTimePassword(String oneTimePassword) { + this.oneTimePassword = oneTimePassword; + } + + public OffsetDateTime getExpiredAt() { + return expiredAt; + } + + public void setExpiredAt(OffsetDateTime expiredAt) { + this.expiredAt = expiredAt; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserEntity.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserEntity.java new file mode 100644 index 0000000..9d2fc23 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserEntity.java @@ -0,0 +1,124 @@ +package org.clevercastle.authforge.impl.postgres.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import org.clevercastle.authforge.core.UserState; + +import java.time.OffsetDateTime; +import java.util.List; + +@Entity +@Table(name = "users") +public class UserEntity { + + @Id + @Column + private String userId; + + @Enumerated(EnumType.STRING) + @Column + private UserState userState; + + @Column + private String hashedPassword; + + @Column + private String resetPasswordCode; + + @Column + private OffsetDateTime resetPasswordCodeExpiredAt; + + @OneToMany(mappedBy = "userId", fetch = FetchType.LAZY) + private List userLoginItems; + + @Column + private OffsetDateTime createdAt; + + @Column + private OffsetDateTime updatedAt; + + @PrePersist + protected void onCreate() { + OffsetDateTime now = OffsetDateTime.now(); + createdAt = now; + updatedAt = now; + } + + @PreUpdate + protected void onUpdate() { + updatedAt = OffsetDateTime.now(); + } + + // Getters and setters + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public UserState getUserState() { + return userState; + } + + public void setUserState(UserState userState) { + this.userState = userState; + } + + public String getHashedPassword() { + return hashedPassword; + } + + public void setHashedPassword(String hashedPassword) { + this.hashedPassword = hashedPassword; + } + + public String getResetPasswordCode() { + return resetPasswordCode; + } + + public void setResetPasswordCode(String resetPasswordCode) { + this.resetPasswordCode = resetPasswordCode; + } + + public OffsetDateTime getResetPasswordCodeExpiredAt() { + return resetPasswordCodeExpiredAt; + } + + public void setResetPasswordCodeExpiredAt(OffsetDateTime resetPasswordCodeExpiredAt) { + this.resetPasswordCodeExpiredAt = resetPasswordCodeExpiredAt; + } + + public List getUserLoginItems() { + return userLoginItems; + } + + public void setUserLoginItems(List userLoginItems) { + this.userLoginItems = userLoginItems; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } + + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(OffsetDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserHmacSecretEntity.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserHmacSecretEntity.java new file mode 100644 index 0000000..d0c7ac1 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserHmacSecretEntity.java @@ -0,0 +1,90 @@ +package org.clevercastle.authforge.impl.postgres.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; + +import java.time.OffsetDateTime; + +@Entity +@Table(name = "user_hmac_secrets") +@IdClass(UserHmacSecretId.class) +public class UserHmacSecretEntity { + + @Id + @Column + private String userId; + + @Id + @Column + private String id; + + @Column + private String secret; + + @Column + private String name; + + @Column + private OffsetDateTime lastUsedAt; + + @Column + private OffsetDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = OffsetDateTime.now(); + } + + // Getters and setters + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public OffsetDateTime getLastUsedAt() { + return lastUsedAt; + } + + public void setLastUsedAt(OffsetDateTime lastUsedAt) { + this.lastUsedAt = lastUsedAt; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserHmacSecretId.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserHmacSecretId.java new file mode 100644 index 0000000..6930b1f --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserHmacSecretId.java @@ -0,0 +1,46 @@ +package org.clevercastle.authforge.impl.postgres.entity; + +import java.io.Serializable; +import java.util.Objects; + +public class UserHmacSecretId implements Serializable { + private String userId; + private String id; + + public UserHmacSecretId() { + } + + public UserHmacSecretId(String userId, String id) { + this.userId = userId; + this.id = id; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserHmacSecretId that = (UserHmacSecretId) o; + return Objects.equals(userId, that.userId) && Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(userId, id); + } +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserLoginItemEntity.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserLoginItemEntity.java new file mode 100644 index 0000000..00d3f19 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserLoginItemEntity.java @@ -0,0 +1,144 @@ +package org.clevercastle.authforge.impl.postgres.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import org.clevercastle.authforge.core.model.UserLoginItem; + +import java.time.OffsetDateTime; + +@Entity +@Table(name = "user_login_items") +public class UserLoginItemEntity { + + @Id + @Column + private String loginIdentifier; + + @Column + private String loginIdentifierPrefix; + + @Enumerated(EnumType.STRING) + @Column + private UserLoginItem.Type type; + + @Column + private String userSub; + + @Column + private String userId; + + @Enumerated(EnumType.STRING) + @Column + private UserLoginItem.State state; + + @Column + private String verificationCode; + + @Column + private OffsetDateTime verificationCodeExpiredAt; + + @Column + private OffsetDateTime createdAt; + + @Column + private OffsetDateTime updatedAt; + + @PrePersist + protected void onCreate() { + OffsetDateTime now = OffsetDateTime.now(); + createdAt = now; + updatedAt = now; + } + + @PreUpdate + protected void onUpdate() { + updatedAt = OffsetDateTime.now(); + } + + // Getters and setters + public String getLoginIdentifier() { + return loginIdentifier; + } + + public void setLoginIdentifier(String loginIdentifier) { + this.loginIdentifier = loginIdentifier; + } + + public String getLoginIdentifierPrefix() { + return loginIdentifierPrefix; + } + + public void setLoginIdentifierPrefix(String loginIdentifierPrefix) { + this.loginIdentifierPrefix = loginIdentifierPrefix; + } + + public UserLoginItem.Type getType() { + return type; + } + + public void setType(UserLoginItem.Type type) { + this.type = type; + } + + public String getUserSub() { + return userSub; + } + + public void setUserSub(String userSub) { + this.userSub = userSub; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public UserLoginItem.State getState() { + return state; + } + + public void setState(UserLoginItem.State state) { + this.state = state; + } + + public String getVerificationCode() { + return verificationCode; + } + + public void setVerificationCode(String verificationCode) { + this.verificationCode = verificationCode; + } + + public OffsetDateTime getVerificationCodeExpiredAt() { + return verificationCodeExpiredAt; + } + + public void setVerificationCodeExpiredAt(OffsetDateTime verificationCodeExpiredAt) { + this.verificationCodeExpiredAt = verificationCodeExpiredAt; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } + + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(OffsetDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserRefreshTokenMappingEntity.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserRefreshTokenMappingEntity.java new file mode 100644 index 0000000..3db9501 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserRefreshTokenMappingEntity.java @@ -0,0 +1,68 @@ +package org.clevercastle.authforge.impl.postgres.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; + +import java.time.OffsetDateTime; + +@Entity +@Table(name = "user_refresh_token_mappings") +@IdClass(UserRefreshTokenMappingId.class) +public class UserRefreshTokenMappingEntity { + + @Id + @Column + private String userId; + + @Id + @Column + private String refreshToken; + + @Column + private OffsetDateTime expiredAt; + + @Column + private OffsetDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = OffsetDateTime.now(); + } + + // Getters and setters + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public OffsetDateTime getExpiredAt() { + return expiredAt; + } + + public void setExpiredAt(OffsetDateTime expiredAt) { + this.expiredAt = expiredAt; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserRefreshTokenMappingId.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserRefreshTokenMappingId.java new file mode 100644 index 0000000..5efa218 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/entity/UserRefreshTokenMappingId.java @@ -0,0 +1,46 @@ +package org.clevercastle.authforge.impl.postgres.entity; + +import java.io.Serializable; +import java.util.Objects; + +public class UserRefreshTokenMappingId implements Serializable { + private String userId; + private String refreshToken; + + public UserRefreshTokenMappingId() { + } + + public UserRefreshTokenMappingId(String userId, String refreshToken) { + this.userId = userId; + this.refreshToken = refreshToken; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserRefreshTokenMappingId that = (UserRefreshTokenMappingId) o; + return Objects.equals(userId, that.userId) && Objects.equals(refreshToken, that.refreshToken); + } + + @Override + public int hashCode() { + return Objects.hash(userId, refreshToken); + } +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/ChallengeSessionMapper.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/ChallengeSessionMapper.java new file mode 100644 index 0000000..73f62ad --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/ChallengeSessionMapper.java @@ -0,0 +1,16 @@ +package org.clevercastle.authforge.impl.postgres.mapper; + +import org.clevercastle.authforge.core.model.ChallengeSession; +import org.clevercastle.authforge.impl.postgres.entity.ChallengeSessionEntity; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface ChallengeSessionMapper { + + ChallengeSessionMapper INSTANCE = Mappers.getMapper(ChallengeSessionMapper.class); + + ChallengeSession toModel(ChallengeSessionEntity entity); + + ChallengeSessionEntity toEntity(ChallengeSession model); +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/OneTimePasswordMapper.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/OneTimePasswordMapper.java new file mode 100644 index 0000000..d45c2b7 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/OneTimePasswordMapper.java @@ -0,0 +1,16 @@ +package org.clevercastle.authforge.impl.postgres.mapper; + +import org.clevercastle.authforge.core.model.OneTimePassword; +import org.clevercastle.authforge.impl.postgres.entity.OneTimePasswordEntity; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface OneTimePasswordMapper { + + OneTimePasswordMapper INSTANCE = Mappers.getMapper(OneTimePasswordMapper.class); + + OneTimePassword toModel(OneTimePasswordEntity entity); + + OneTimePasswordEntity toEntity(OneTimePassword model); +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserHmacSecretMapper.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserHmacSecretMapper.java new file mode 100644 index 0000000..c9ccce8 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserHmacSecretMapper.java @@ -0,0 +1,20 @@ +package org.clevercastle.authforge.impl.postgres.mapper; + +import org.clevercastle.authforge.core.model.UserHmacSecret; +import org.clevercastle.authforge.impl.postgres.entity.UserHmacSecretEntity; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface UserHmacSecretMapper { + + UserHmacSecretMapper INSTANCE = Mappers.getMapper(UserHmacSecretMapper.class); + + UserHmacSecret toModel(UserHmacSecretEntity entity); + + UserHmacSecretEntity toEntity(UserHmacSecret model); + + List toModelList(List entities); +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserLoginItemMapper.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserLoginItemMapper.java new file mode 100644 index 0000000..53f16ec --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserLoginItemMapper.java @@ -0,0 +1,16 @@ +package org.clevercastle.authforge.impl.postgres.mapper; + +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.impl.postgres.entity.UserLoginItemEntity; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface UserLoginItemMapper { + + UserLoginItemMapper INSTANCE = Mappers.getMapper(UserLoginItemMapper.class); + + UserLoginItem toModel(UserLoginItemEntity entity); + + UserLoginItemEntity toEntity(UserLoginItem model); +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserMapper.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserMapper.java new file mode 100644 index 0000000..b2fd85b --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserMapper.java @@ -0,0 +1,16 @@ +package org.clevercastle.authforge.impl.postgres.mapper; + +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.impl.postgres.entity.UserEntity; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface UserMapper { + + UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); + + User toModel(UserEntity entity); + + UserEntity toEntity(User model); +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserRefreshTokenMappingMapper.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserRefreshTokenMappingMapper.java new file mode 100644 index 0000000..f0bf17c --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/mapper/UserRefreshTokenMappingMapper.java @@ -0,0 +1,16 @@ +package org.clevercastle.authforge.impl.postgres.mapper; + +import org.clevercastle.authforge.core.model.UserRefreshTokenMapping; +import org.clevercastle.authforge.impl.postgres.entity.UserRefreshTokenMappingEntity; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface UserRefreshTokenMappingMapper { + + UserRefreshTokenMappingMapper INSTANCE = Mappers.getMapper(UserRefreshTokenMappingMapper.class); + + UserRefreshTokenMapping toModel(UserRefreshTokenMappingEntity entity); + + UserRefreshTokenMappingEntity toEntity(UserRefreshTokenMapping model); +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresChallengeSessionRepository.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresChallengeSessionRepository.java new file mode 100644 index 0000000..2a6cce2 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresChallengeSessionRepository.java @@ -0,0 +1,64 @@ +package org.clevercastle.authforge.impl.postgres.repository; + +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.ChallengeSession; +import org.clevercastle.authforge.core.repository.ChallengeSessionRepository; +import org.clevercastle.authforge.impl.postgres.entity.ChallengeSessionEntity; +import org.clevercastle.authforge.impl.postgres.mapper.ChallengeSessionMapper; +import org.clevercastle.authforge.impl.postgres.repository.jpa.ChallengeSessionJpaRepository; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; + +public class PostgresChallengeSessionRepository implements ChallengeSessionRepository { + + private final ChallengeSessionJpaRepository challengeSessionJpaRepository; + + public PostgresChallengeSessionRepository(JpaRepositoryFactory jpaRepositoryFactory) { + this.challengeSessionJpaRepository = jpaRepositoryFactory.getRepository(ChallengeSessionJpaRepository.class); + } + + @Override + public void createChallenge(ChallengeSession session) throws CastleException { + try { + ChallengeSessionEntity entity = ChallengeSessionMapper.INSTANCE.toEntity(session); + challengeSessionJpaRepository.save(entity); + } catch (Exception e) { + throw new CastleException("Failed to create challenge session: " + e.getMessage(), e); + } + } + + @Override + public ChallengeSession getById(String id) throws CastleException { + try { + ChallengeSessionEntity entity = challengeSessionJpaRepository.findById(id) + .orElseThrow(() -> new CastleException("Challenge session not found with id: " + id)); + return ChallengeSessionMapper.INSTANCE.toModel(entity); + } catch (CastleException e) { + throw e; + } catch (Exception e) { + throw new CastleException("Failed to get challenge session by id: " + e.getMessage(), e); + } + } + + @Override + public void markVerified(String id) throws CastleException { + try { + int updated = challengeSessionJpaRepository.markVerified(id); + if (updated == 0) { + throw new CastleException("Challenge session not found with id: " + id); + } + } catch (CastleException e) { + throw e; + } catch (Exception e) { + throw new CastleException("Failed to mark challenge session as verified: " + e.getMessage(), e); + } + } + + @Override + public void delete(String id) throws CastleException { + try { + challengeSessionJpaRepository.deleteById(id); + } catch (Exception e) { + throw new CastleException("Failed to delete challenge session: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresLoginItemRepository.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresLoginItemRepository.java new file mode 100644 index 0000000..262183d --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresLoginItemRepository.java @@ -0,0 +1,65 @@ +package org.clevercastle.authforge.impl.postgres.repository; + +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.repository.UserLoginItemRepository; +import org.clevercastle.authforge.impl.postgres.entity.UserLoginItemEntity; +import org.clevercastle.authforge.impl.postgres.mapper.UserLoginItemMapper; +import org.clevercastle.authforge.impl.postgres.repository.jpa.UserLoginItemJpaRepository; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; + +import java.util.Optional; + +public class PostgresLoginItemRepository implements UserLoginItemRepository { + + private final UserLoginItemJpaRepository userLoginItemJpaRepository; + + public PostgresLoginItemRepository(JpaRepositoryFactory jpaRepositoryFactory) { + this.userLoginItemJpaRepository = jpaRepositoryFactory.getRepository(UserLoginItemJpaRepository.class); + } + + @Override + public void confirmLoginItem(String loginIdentifier) throws CastleException { + try { + int updated = userLoginItemJpaRepository.confirmLoginItem(loginIdentifier); + if (updated == 0) { + throw new CastleException("No login item found with identifier: " + loginIdentifier); + } + } catch (CastleException e) { + throw e; + } catch (Exception e) { + throw new CastleException("Failed to confirm login item: " + e.getMessage(), e); + } + } + + @Override + public UserLoginItem save(UserLoginItem item) throws CastleException { + try { + UserLoginItemEntity entity = UserLoginItemMapper.INSTANCE.toEntity(item); + UserLoginItemEntity savedEntity = userLoginItemJpaRepository.save(entity); + return UserLoginItemMapper.INSTANCE.toModel(savedEntity); + } catch (Exception e) { + throw new CastleException("Failed to save login item: " + e.getMessage(), e); + } + } + + @Override + public UserLoginItem getByLoginIdentifier(String loginIdentifier) throws CastleException { + try { + Optional entity = userLoginItemJpaRepository.findById(loginIdentifier); + return entity.map(UserLoginItemMapper.INSTANCE::toModel).orElse(null); + } catch (Exception e) { + throw new CastleException("Failed to get login item by identifier: " + e.getMessage(), e); + } + } + + @Override + public UserLoginItem getByUserSub(String userSub) throws CastleException { + try { + Optional entity = userLoginItemJpaRepository.findByUserSub(userSub); + return entity.map(UserLoginItemMapper.INSTANCE::toModel).orElse(null); + } catch (Exception e) { + throw new CastleException("Failed to get login item by userSub: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresOneTimePasswordRepository.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresOneTimePasswordRepository.java new file mode 100644 index 0000000..a905c7f --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresOneTimePasswordRepository.java @@ -0,0 +1,43 @@ +package org.clevercastle.authforge.impl.postgres.repository; + +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.OneTimePassword; +import org.clevercastle.authforge.core.repository.OneTimePasswordRepository; +import org.clevercastle.authforge.impl.postgres.entity.OneTimePasswordEntity; +import org.clevercastle.authforge.impl.postgres.mapper.OneTimePasswordMapper; +import org.clevercastle.authforge.impl.postgres.repository.jpa.OneTimePasswordJpaRepository; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; + +public class PostgresOneTimePasswordRepository implements OneTimePasswordRepository { + + private final OneTimePasswordJpaRepository otpJpaRepository; + + public PostgresOneTimePasswordRepository(JpaRepositoryFactory jpaRepositoryFactory) { + this.otpJpaRepository = jpaRepositoryFactory.getRepository(OneTimePasswordJpaRepository.class); + } + + @Override + public void saveOneTimePassword(OneTimePassword userOneTimePasswordMapping) throws CastleException { + try { + OneTimePasswordEntity entity = OneTimePasswordMapper.INSTANCE.toEntity(userOneTimePasswordMapping); + otpJpaRepository.save(entity); + } catch (Exception e) { + throw new CastleException("Failed to save one-time password: " + e.getMessage(), e); + } + } + + @Override + public boolean verifyOneTimePassword(String loginIdentifier, String oneTimePassword) throws CastleException { + try { + boolean exists = otpJpaRepository.existsByLoginIdentifierAndOneTimePassword(loginIdentifier, oneTimePassword); + if (exists) { + // Remove the OTP after verification (single-use) + int deleted = otpJpaRepository.deleteByLoginIdentifierAndOneTimePassword(loginIdentifier, oneTimePassword); + return deleted > 0; + } + return false; + } catch (Exception e) { + throw new CastleException("Failed to verify one-time password: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresRefreshTokenRepository.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresRefreshTokenRepository.java new file mode 100644 index 0000000..1d1a644 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresRefreshTokenRepository.java @@ -0,0 +1,53 @@ +package org.clevercastle.authforge.impl.postgres.repository; + +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserRefreshTokenMapping; +import org.clevercastle.authforge.core.repository.RefreshTokenRepository; +import org.clevercastle.authforge.impl.postgres.entity.UserRefreshTokenMappingEntity; +import org.clevercastle.authforge.impl.postgres.mapper.UserRefreshTokenMappingMapper; +import org.clevercastle.authforge.impl.postgres.repository.jpa.UserRefreshTokenMappingJpaRepository; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; + +import java.time.OffsetDateTime; + +public class PostgresRefreshTokenRepository implements RefreshTokenRepository { + + private final UserRefreshTokenMappingJpaRepository refreshTokenJpaRepository; + + public PostgresRefreshTokenRepository(JpaRepositoryFactory jpaRepositoryFactory) { + this.refreshTokenJpaRepository = jpaRepositoryFactory.getRepository(UserRefreshTokenMappingJpaRepository.class); + } + + @Override + public UserRefreshTokenMapping addRefreshToken(User user, String refreshToken, OffsetDateTime expiredAt) throws CastleException { + try { + UserRefreshTokenMapping mapping = new UserRefreshTokenMapping(); + mapping.setUserId(user.getUserId()); + mapping.setRefreshToken(refreshToken); + mapping.setExpiredAt(expiredAt); + mapping.setCreatedAt(OffsetDateTime.now()); + + UserRefreshTokenMappingEntity entity = UserRefreshTokenMappingMapper.INSTANCE.toEntity(mapping); + UserRefreshTokenMappingEntity savedEntity = refreshTokenJpaRepository.save(entity); + return UserRefreshTokenMappingMapper.INSTANCE.toModel(savedEntity); + } catch (Exception e) { + throw new CastleException("Failed to add refresh token: " + e.getMessage(), e); + } + } + + @Override + public boolean verifyRefreshToken(User user, String refreshToken) throws CastleException { + try { + boolean exists = refreshTokenJpaRepository.existsByUserIdAndRefreshToken(user.getUserId(), refreshToken); + if (exists) { + // Remove the token after verification (single-use) + int deleted = refreshTokenJpaRepository.deleteByUserIdAndRefreshToken(user.getUserId(), refreshToken); + return deleted > 0; + } + return false; + } catch (Exception e) { + throw new CastleException("Failed to verify refresh token: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresUserHmacSecretRepository.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresUserHmacSecretRepository.java new file mode 100644 index 0000000..1ab5b0c --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresUserHmacSecretRepository.java @@ -0,0 +1,64 @@ +package org.clevercastle.authforge.impl.postgres.repository; + +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.UserHmacSecret; +import org.clevercastle.authforge.core.repository.UserHmacSecretRepository; +import org.clevercastle.authforge.impl.postgres.entity.UserHmacSecretEntity; +import org.clevercastle.authforge.impl.postgres.mapper.UserHmacSecretMapper; +import org.clevercastle.authforge.impl.postgres.repository.jpa.UserHmacSecretJpaRepository; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; + +import java.time.OffsetDateTime; +import java.util.List; + +public class PostgresUserHmacSecretRepository implements UserHmacSecretRepository { + + private final UserHmacSecretJpaRepository hmacSecretJpaRepository; + + public PostgresUserHmacSecretRepository(JpaRepositoryFactory jpaRepositoryFactory) { + this.hmacSecretJpaRepository = jpaRepositoryFactory.getRepository(UserHmacSecretJpaRepository.class); + } + + @Override + public void createHmacSecret(UserHmacSecret userHmacSecret) throws CastleException { + try { + UserHmacSecretEntity entity = UserHmacSecretMapper.INSTANCE.toEntity(userHmacSecret); + hmacSecretJpaRepository.save(entity); + } catch (Exception e) { + throw new CastleException("Failed to create HMAC secret: " + e.getMessage(), e); + } + } + + @Override + public List listHmacSecretByUserId(String userId) throws CastleException { + try { + List entities = hmacSecretJpaRepository.findByUserId(userId); + return UserHmacSecretMapper.INSTANCE.toModelList(entities); + } catch (Exception e) { + throw new CastleException("Failed to list HMAC secrets: " + e.getMessage(), e); + } + } + + @Override + public void deleteHmacSecret(String userId, String id) throws CastleException { + try { + hmacSecretJpaRepository.deleteByUserIdAndId(userId, id); + } catch (Exception e) { + throw new CastleException("Failed to delete HMAC secret: " + e.getMessage(), e); + } + } + + @Override + public void touchLastUsedAt(String userId, String id) throws CastleException { + try { + int updated = hmacSecretJpaRepository.updateLastUsedAt(userId, id, OffsetDateTime.now()); + if (updated == 0) { + throw new CastleException("HMAC secret not found with userId: " + userId + " and id: " + id); + } + } catch (CastleException e) { + throw e; + } catch (Exception e) { + throw new CastleException("Failed to touch last used at: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresUserModelRepository.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresUserModelRepository.java new file mode 100644 index 0000000..9299451 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresUserModelRepository.java @@ -0,0 +1,42 @@ +package org.clevercastle.authforge.impl.postgres.repository; + +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.repository.UserRepository; +import org.clevercastle.authforge.impl.postgres.entity.UserEntity; +import org.clevercastle.authforge.impl.postgres.mapper.UserMapper; +import org.clevercastle.authforge.impl.postgres.repository.jpa.UserJpaRepository; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; + +public class PostgresUserModelRepository implements UserRepository { + + private final UserJpaRepository userJpaRepository; + + public PostgresUserModelRepository(JpaRepositoryFactory jpaRepositoryFactory) { + this.userJpaRepository = jpaRepositoryFactory.getRepository(UserJpaRepository.class); + } + + @Override + public User save(User user) throws CastleException { + try { + UserEntity entity = UserMapper.INSTANCE.toEntity(user); + UserEntity savedEntity = userJpaRepository.save(entity); + return UserMapper.INSTANCE.toModel(savedEntity); + } catch (Exception e) { + throw new CastleException("Failed to save user: " + e.getMessage(), e); + } + } + + @Override + public User getByUserId(String userId) throws CastleException { + try { + UserEntity entity = userJpaRepository.findById(userId) + .orElseThrow(() -> new CastleException("User not found with id: " + userId)); + return UserMapper.INSTANCE.toModel(entity); + } catch (CastleException e) { + throw e; + } catch (Exception e) { + throw new CastleException("Failed to get user by id: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresUserRegistrationRepository.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresUserRegistrationRepository.java new file mode 100644 index 0000000..2288e5f --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/PostgresUserRegistrationRepository.java @@ -0,0 +1,39 @@ +package org.clevercastle.authforge.impl.postgres.repository; + +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.repository.UserRegistrationRepository; +import org.clevercastle.authforge.impl.postgres.entity.UserEntity; +import org.clevercastle.authforge.impl.postgres.entity.UserLoginItemEntity; +import org.clevercastle.authforge.impl.postgres.mapper.UserLoginItemMapper; +import org.clevercastle.authforge.impl.postgres.mapper.UserMapper; +import org.clevercastle.authforge.impl.postgres.repository.jpa.UserJpaRepository; +import org.clevercastle.authforge.impl.postgres.repository.jpa.UserLoginItemJpaRepository; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; + +public class PostgresUserRegistrationRepository implements UserRegistrationRepository { + + private final UserJpaRepository userJpaRepository; + private final UserLoginItemJpaRepository userLoginItemJpaRepository; + + public PostgresUserRegistrationRepository(JpaRepositoryFactory jpaRepositoryFactory) { + this.userJpaRepository = jpaRepositoryFactory.getRepository(UserJpaRepository.class); + this.userLoginItemJpaRepository = jpaRepositoryFactory.getRepository(UserLoginItemJpaRepository.class); + } + + @Override + public void save(User user, UserLoginItem userLoginItem) throws CastleException { + try { + // Save user first + UserEntity userEntity = UserMapper.INSTANCE.toEntity(user); + userJpaRepository.save(userEntity); + + // Save login item + UserLoginItemEntity loginItemEntity = UserLoginItemMapper.INSTANCE.toEntity(userLoginItem); + userLoginItemJpaRepository.save(loginItemEntity); + } catch (Exception e) { + throw new CastleException("Failed to save user registration: " + e.getMessage(), e); + } + } +} diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/ChallengeSessionJpaRepository.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/ChallengeSessionJpaRepository.java new file mode 100644 index 0000000..27720c9 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/ChallengeSessionJpaRepository.java @@ -0,0 +1,14 @@ +package org.clevercastle.authforge.impl.postgres.repository.jpa; + +import org.clevercastle.authforge.impl.postgres.entity.ChallengeSessionEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ChallengeSessionJpaRepository extends JpaRepository { + + @Modifying + @Query("UPDATE ChallengeSessionEntity c SET c.verified = true WHERE c.id = :id") + int markVerified(@Param("id") String id); +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/OneTimePasswordJpaRepository.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/OneTimePasswordJpaRepository.java new file mode 100644 index 0000000..25dd0d9 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/OneTimePasswordJpaRepository.java @@ -0,0 +1,17 @@ +package org.clevercastle.authforge.impl.postgres.repository.jpa; + +import org.clevercastle.authforge.impl.postgres.entity.OneTimePasswordEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface OneTimePasswordJpaRepository extends JpaRepository { + + @Modifying + @Query("DELETE FROM OneTimePasswordEntity o WHERE o.loginIdentifier = :loginIdentifier AND o.oneTimePassword = :oneTimePassword") + int deleteByLoginIdentifierAndOneTimePassword(@Param("loginIdentifier") String loginIdentifier, + @Param("oneTimePassword") String oneTimePassword); + + boolean existsByLoginIdentifierAndOneTimePassword(String loginIdentifier, String oneTimePassword); +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserHmacSecretJpaRepository.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserHmacSecretJpaRepository.java new file mode 100644 index 0000000..6caf9cc --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserHmacSecretJpaRepository.java @@ -0,0 +1,22 @@ +package org.clevercastle.authforge.impl.postgres.repository.jpa; + +import org.clevercastle.authforge.impl.postgres.entity.UserHmacSecretEntity; +import org.clevercastle.authforge.impl.postgres.entity.UserHmacSecretId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.OffsetDateTime; +import java.util.List; + +public interface UserHmacSecretJpaRepository extends JpaRepository { + + List findByUserId(String userId); + + @Modifying + @Query("UPDATE UserHmacSecretEntity u SET u.lastUsedAt = :lastUsedAt WHERE u.userId = :userId AND u.id = :id") + int updateLastUsedAt(@Param("userId") String userId, @Param("id") String id, @Param("lastUsedAt") OffsetDateTime lastUsedAt); + + void deleteByUserIdAndId(String userId, String id); +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserJpaRepository.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserJpaRepository.java new file mode 100644 index 0000000..f16b038 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserJpaRepository.java @@ -0,0 +1,7 @@ +package org.clevercastle.authforge.impl.postgres.repository.jpa; + +import org.clevercastle.authforge.impl.postgres.entity.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserJpaRepository extends JpaRepository { +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserLoginItemJpaRepository.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserLoginItemJpaRepository.java new file mode 100644 index 0000000..fc754f0 --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserLoginItemJpaRepository.java @@ -0,0 +1,18 @@ +package org.clevercastle.authforge.impl.postgres.repository.jpa; + +import org.clevercastle.authforge.impl.postgres.entity.UserLoginItemEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface UserLoginItemJpaRepository extends JpaRepository { + + Optional findByUserSub(String userSub); + + @Modifying + @Query("UPDATE UserLoginItemEntity u SET u.state = org.clevercastle.authforge.core.model.UserLoginItem$State.ACTIVE WHERE u.loginIdentifier = :loginIdentifier") + int confirmLoginItem(@Param("loginIdentifier") String loginIdentifier); +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserRefreshTokenMappingJpaRepository.java b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserRefreshTokenMappingJpaRepository.java new file mode 100644 index 0000000..9f069ef --- /dev/null +++ b/impls/impl-postgres/src/main/java/org/clevercastle/authforge/impl/postgres/repository/jpa/UserRefreshTokenMappingJpaRepository.java @@ -0,0 +1,17 @@ +package org.clevercastle.authforge.impl.postgres.repository.jpa; + +import org.clevercastle.authforge.impl.postgres.entity.UserRefreshTokenMappingEntity; +import org.clevercastle.authforge.impl.postgres.entity.UserRefreshTokenMappingId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface UserRefreshTokenMappingJpaRepository extends JpaRepository { + + @Modifying + @Query("DELETE FROM UserRefreshTokenMappingEntity u WHERE u.userId = :userId AND u.refreshToken = :refreshToken") + int deleteByUserIdAndRefreshToken(@Param("userId") String userId, @Param("refreshToken") String refreshToken); + + boolean existsByUserIdAndRefreshToken(String userId, String refreshToken); +} \ No newline at end of file diff --git a/impls/impl-postgres/src/main/resources/application-postgres.properties b/impls/impl-postgres/src/main/resources/application-postgres.properties new file mode 100644 index 0000000..d4afd50 --- /dev/null +++ b/impls/impl-postgres/src/main/resources/application-postgres.properties @@ -0,0 +1,19 @@ +# PostgreSQL Database Configuration +spring.datasource.url=jdbc:postgresql://localhost:5432/authforge +spring.datasource.username=authforge_user +spring.datasource.password=authforge_password +spring.datasource.driver-class-name=org.postgresql.Driver +# JPA Configuration +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=false +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.use_sql_comments=true +# Hibernate Naming Strategy - 自动转换为 snake_case +spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy +spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl +# Connection Pool Configuration +spring.datasource.hikari.maximum-pool-size=20 +spring.datasource.hikari.minimum-idle=5 +spring.datasource.hikari.idle-timeout=300000 +spring.datasource.hikari.connection-timeout=30000 \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 4448bf6..560f0d7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,7 @@ rootProject.name = "auth-forge" -include("examples:spring-boot-example") include("core") include("impls:impl-dynamodb") -include("impls:impl-postgres") \ No newline at end of file +include("impls:impl-postgres") +include("examples:spring-boot-example")