From 4017074731aabea58f3c4d077a4840ab861f1a8b Mon Sep 17 00:00:00 2001 From: ivyxjc Date: Wed, 30 Apr 2025 16:54:40 +0800 Subject: [PATCH 1/7] fix(): oauth2 user still use customize user sub --- src/main/java/org/clevercastle/authforge/UserServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/clevercastle/authforge/UserServiceImpl.java b/src/main/java/org/clevercastle/authforge/UserServiceImpl.java index 22fc8ba..7a03431 100644 --- a/src/main/java/org/clevercastle/authforge/UserServiceImpl.java +++ b/src/main/java/org/clevercastle/authforge/UserServiceImpl.java @@ -152,7 +152,7 @@ public UserWithToken exchange(Oauth2ClientConfig clientConfig, String authorizat userLoginItem = new UserLoginItem(); userLoginItem.setUserId(userId); userLoginItem.setLoginIdentifier(oauth2User.getLoginIdentifier()); - userLoginItem.setUserSub(oauth2User.getUserSub()); + userLoginItem.setUserSub(UUID.randomUUID().toString()); userLoginItem.setCreatedAt(now); userLoginItem.setUpdatedAt(now); this.userRepository.save(user, userLoginItem); From 09605e9219df980c863d8b0431ab8d8e0daaee50 Mon Sep 17 00:00:00 2001 From: ivyxjc Date: Tue, 13 May 2025 17:36:50 +0800 Subject: [PATCH 2/7] refactor(): refactor package structure --- .../java/org/clevercastle/authforge/UserService.java | 4 ++-- .../org/clevercastle/authforge/UserServiceImpl.java | 4 ++-- .../org/clevercastle/authforge/UserWithToken.java | 2 +- .../authforge/{entity => model}/User.java | 2 +- .../authforge/{entity => model}/UserLoginItem.java | 2 +- .../{entity => model}/UserRefreshTokenMapping.java | 2 +- .../authforge/repository/UserRepository.java | 6 +++--- .../authforge/repository/dynamodb/DynamodbUser.java | 12 +++++++++++- .../dynamodb/DynamodbUserRepositoryImpl.java | 6 +++--- .../repository/dynamodb/DynamodbUserUtil.java | 6 +++--- .../rdsjpa/RdsJpaUserLoginItemRepository.java | 2 +- .../repository/rdsjpa/RdsJpaUserModelRepository.java | 2 +- .../RdsJpaUserRefreshTokenMappingRepository.java | 2 +- .../repository/rdsjpa/RdsJpaUserRepositoryImpl.java | 6 +++--- .../clevercastle/authforge/token/TokenService.java | 4 ++-- .../authforge/token/jwt/JwtTokenService.java | 4 ++-- .../verification/AbstractVerificationService.java | 4 ++-- 17 files changed, 40 insertions(+), 30 deletions(-) rename src/main/java/org/clevercastle/authforge/{entity => model}/User.java (98%) rename src/main/java/org/clevercastle/authforge/{entity => model}/UserLoginItem.java (98%) rename src/main/java/org/clevercastle/authforge/{entity => model}/UserRefreshTokenMapping.java (97%) diff --git a/src/main/java/org/clevercastle/authforge/UserService.java b/src/main/java/org/clevercastle/authforge/UserService.java index c522827..b6276ad 100644 --- a/src/main/java/org/clevercastle/authforge/UserService.java +++ b/src/main/java/org/clevercastle/authforge/UserService.java @@ -1,8 +1,8 @@ package org.clevercastle.authforge; import org.apache.commons.lang3.tuple.Pair; -import org.clevercastle.authforge.entity.User; -import org.clevercastle.authforge.entity.UserLoginItem; +import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.model.UserLoginItem; import org.clevercastle.authforge.exception.CastleException; import org.clevercastle.authforge.oauth2.Oauth2ClientConfig; diff --git a/src/main/java/org/clevercastle/authforge/UserServiceImpl.java b/src/main/java/org/clevercastle/authforge/UserServiceImpl.java index 7a03431..b9a79d6 100644 --- a/src/main/java/org/clevercastle/authforge/UserServiceImpl.java +++ b/src/main/java/org/clevercastle/authforge/UserServiceImpl.java @@ -17,8 +17,8 @@ import org.clevercastle.authforge.exception.CastleException; import org.clevercastle.authforge.exception.UserExistException; import org.clevercastle.authforge.exception.UserNotFoundException; -import org.clevercastle.authforge.entity.User; -import org.clevercastle.authforge.entity.UserLoginItem; +import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.model.UserLoginItem; import org.clevercastle.authforge.oauth2.Oauth2ClientConfig; import org.clevercastle.authforge.oauth2.Oauth2User; import org.clevercastle.authforge.repository.UserRepository; diff --git a/src/main/java/org/clevercastle/authforge/UserWithToken.java b/src/main/java/org/clevercastle/authforge/UserWithToken.java index 71a4c7e..73f9e2a 100644 --- a/src/main/java/org/clevercastle/authforge/UserWithToken.java +++ b/src/main/java/org/clevercastle/authforge/UserWithToken.java @@ -1,6 +1,6 @@ package org.clevercastle.authforge; -import org.clevercastle.authforge.entity.User; +import org.clevercastle.authforge.model.User; public class UserWithToken { private final User user; diff --git a/src/main/java/org/clevercastle/authforge/entity/User.java b/src/main/java/org/clevercastle/authforge/model/User.java similarity index 98% rename from src/main/java/org/clevercastle/authforge/entity/User.java rename to src/main/java/org/clevercastle/authforge/model/User.java index 503a5b7..c7bdb00 100644 --- a/src/main/java/org/clevercastle/authforge/entity/User.java +++ b/src/main/java/org/clevercastle/authforge/model/User.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.entity; +package org.clevercastle.authforge.model; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; diff --git a/src/main/java/org/clevercastle/authforge/entity/UserLoginItem.java b/src/main/java/org/clevercastle/authforge/model/UserLoginItem.java similarity index 98% rename from src/main/java/org/clevercastle/authforge/entity/UserLoginItem.java rename to src/main/java/org/clevercastle/authforge/model/UserLoginItem.java index 27bfe1b..8b5754c 100644 --- a/src/main/java/org/clevercastle/authforge/entity/UserLoginItem.java +++ b/src/main/java/org/clevercastle/authforge/model/UserLoginItem.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.entity; +package org.clevercastle.authforge.model; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; diff --git a/src/main/java/org/clevercastle/authforge/entity/UserRefreshTokenMapping.java b/src/main/java/org/clevercastle/authforge/model/UserRefreshTokenMapping.java similarity index 97% rename from src/main/java/org/clevercastle/authforge/entity/UserRefreshTokenMapping.java rename to src/main/java/org/clevercastle/authforge/model/UserRefreshTokenMapping.java index 68be72e..91e3ee7 100644 --- a/src/main/java/org/clevercastle/authforge/entity/UserRefreshTokenMapping.java +++ b/src/main/java/org/clevercastle/authforge/model/UserRefreshTokenMapping.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.entity; +package org.clevercastle.authforge.model; import jakarta.persistence.Entity; import jakarta.persistence.Id; diff --git a/src/main/java/org/clevercastle/authforge/repository/UserRepository.java b/src/main/java/org/clevercastle/authforge/repository/UserRepository.java index 166e3a6..27c680b 100644 --- a/src/main/java/org/clevercastle/authforge/repository/UserRepository.java +++ b/src/main/java/org/clevercastle/authforge/repository/UserRepository.java @@ -2,9 +2,9 @@ import jakarta.annotation.Nonnull; import org.apache.commons.lang3.tuple.Pair; -import org.clevercastle.authforge.entity.User; -import org.clevercastle.authforge.entity.UserLoginItem; -import org.clevercastle.authforge.entity.UserRefreshTokenMapping; +import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.model.UserLoginItem; +import org.clevercastle.authforge.model.UserRefreshTokenMapping; import org.clevercastle.authforge.exception.CastleException; import java.time.OffsetDateTime; diff --git a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUser.java b/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUser.java index 57fcb60..d51b11c 100644 --- a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUser.java +++ b/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUser.java @@ -1,7 +1,7 @@ package org.clevercastle.authforge.repository.dynamodb; import org.clevercastle.authforge.UserState; -import org.clevercastle.authforge.entity.UserLoginItem; +import org.clevercastle.authforge.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; @@ -29,6 +29,8 @@ public enum Type { private String pk; private String sk; + private Type type; + // region from user table private UserState userState; private String userHashedPassword; @@ -69,6 +71,14 @@ 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; } diff --git a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java b/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java index 192ee42..8c6a551 100644 --- a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java +++ b/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java @@ -3,9 +3,9 @@ import jakarta.annotation.Nonnull; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; -import org.clevercastle.authforge.entity.User; -import org.clevercastle.authforge.entity.UserLoginItem; -import org.clevercastle.authforge.entity.UserRefreshTokenMapping; +import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.model.UserLoginItem; +import org.clevercastle.authforge.model.UserRefreshTokenMapping; import org.clevercastle.authforge.exception.CastleException; import org.clevercastle.authforge.repository.UserRepository; import org.clevercastle.authforge.util.TimeUtils; diff --git a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserUtil.java b/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserUtil.java index 37c6e8f..8349aa0 100644 --- a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserUtil.java +++ b/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserUtil.java @@ -1,8 +1,8 @@ package org.clevercastle.authforge.repository.dynamodb; -import org.clevercastle.authforge.entity.User; -import org.clevercastle.authforge.entity.UserLoginItem; -import org.clevercastle.authforge.entity.UserRefreshTokenMapping; +import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.model.UserLoginItem; +import org.clevercastle.authforge.model.UserRefreshTokenMapping; public class DynamodbUserUtil { diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserLoginItemRepository.java b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserLoginItemRepository.java index e636d2f..4401354 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserLoginItemRepository.java +++ b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserLoginItemRepository.java @@ -1,6 +1,6 @@ package org.clevercastle.authforge.repository.rdsjpa; -import org.clevercastle.authforge.entity.UserLoginItem; +import org.clevercastle.authforge.model.UserLoginItem; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserModelRepository.java b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserModelRepository.java index 55b65a3..1b5c8cb 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserModelRepository.java +++ b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserModelRepository.java @@ -1,6 +1,6 @@ package org.clevercastle.authforge.repository.rdsjpa; -import org.clevercastle.authforge.entity.User; +import org.clevercastle.authforge.model.User; public interface RdsJpaUserModelRepository { User save(User user); diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRefreshTokenMappingRepository.java b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRefreshTokenMappingRepository.java index 5ba0413..661e8dd 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRefreshTokenMappingRepository.java +++ b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRefreshTokenMappingRepository.java @@ -1,6 +1,6 @@ package org.clevercastle.authforge.repository.rdsjpa; -import org.clevercastle.authforge.entity.UserRefreshTokenMapping; +import org.clevercastle.authforge.model.UserRefreshTokenMapping; public interface RdsJpaUserRefreshTokenMappingRepository { UserRefreshTokenMapping getByUserIdAndRefreshToken(String userIed, String refreshToken); diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRepositoryImpl.java b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRepositoryImpl.java index 19f21ab..9ae1d22 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRepositoryImpl.java +++ b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRepositoryImpl.java @@ -4,9 +4,9 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.clevercastle.authforge.exception.CastleException; -import org.clevercastle.authforge.entity.User; -import org.clevercastle.authforge.entity.UserLoginItem; -import org.clevercastle.authforge.entity.UserRefreshTokenMapping; +import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.model.UserLoginItem; +import org.clevercastle.authforge.model.UserRefreshTokenMapping; import org.clevercastle.authforge.repository.UserRepository; import org.clevercastle.authforge.util.TimeUtils; diff --git a/src/main/java/org/clevercastle/authforge/token/TokenService.java b/src/main/java/org/clevercastle/authforge/token/TokenService.java index 8bce300..496e2a7 100644 --- a/src/main/java/org/clevercastle/authforge/token/TokenService.java +++ b/src/main/java/org/clevercastle/authforge/token/TokenService.java @@ -2,8 +2,8 @@ import org.clevercastle.authforge.exception.CastleException; import org.clevercastle.authforge.TokenHolder; -import org.clevercastle.authforge.entity.User; -import org.clevercastle.authforge.entity.UserLoginItem; +import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.model.UserLoginItem; public interface TokenService { enum Scope { diff --git a/src/main/java/org/clevercastle/authforge/token/jwt/JwtTokenService.java b/src/main/java/org/clevercastle/authforge/token/jwt/JwtTokenService.java index e0b8d92..40e9fc2 100644 --- a/src/main/java/org/clevercastle/authforge/token/jwt/JwtTokenService.java +++ b/src/main/java/org/clevercastle/authforge/token/jwt/JwtTokenService.java @@ -4,8 +4,8 @@ import org.clevercastle.authforge.exception.CastleException; import org.clevercastle.authforge.Config; import org.clevercastle.authforge.TokenHolder; -import org.clevercastle.authforge.entity.User; -import org.clevercastle.authforge.entity.UserLoginItem; +import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.model.UserLoginItem; import org.clevercastle.authforge.token.TokenService; import org.clevercastle.authforge.util.JsonUtil; import org.clevercastle.authforge.util.TimeUtils; diff --git a/src/main/java/org/clevercastle/authforge/verification/AbstractVerificationService.java b/src/main/java/org/clevercastle/authforge/verification/AbstractVerificationService.java index 38d21da..5328990 100644 --- a/src/main/java/org/clevercastle/authforge/verification/AbstractVerificationService.java +++ b/src/main/java/org/clevercastle/authforge/verification/AbstractVerificationService.java @@ -3,8 +3,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.clevercastle.authforge.exception.CastleException; -import org.clevercastle.authforge.entity.User; -import org.clevercastle.authforge.entity.UserLoginItem; +import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.model.UserLoginItem; import org.clevercastle.authforge.repository.UserRepository; import org.clevercastle.authforge.util.TimeUtils; From 4a8bab50bbe0dc83b67993a170527f73c2d1199e Mon Sep 17 00:00:00 2001 From: ivyxjc Date: Tue, 13 May 2025 17:36:59 +0800 Subject: [PATCH 3/7] refactor(example): refactor package structure --- .../springboot/springbootexample/AuthController.java | 5 +++-- .../springbootexample/SpringBootExampleApplication.java | 2 +- .../springbootexample/UserLoginItemRepositoryAdapter.java | 2 +- .../springbootexample/UserModelRepositoryAdapter.java | 2 +- .../UserRefreshTokenMappingRepositoryAdapter.java | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/AuthController.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/AuthController.java index ffa9351..3c51a83 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/AuthController.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/AuthController.java @@ -5,8 +5,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.clevercastle.authforge.exception.CastleException; -import org.clevercastle.authforge.entity.User; -import org.clevercastle.authforge.entity.UserLoginItem; +import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.model.UserLoginItem; import org.clevercastle.authforge.UserRegisterRequest; import org.clevercastle.authforge.UserService; import org.clevercastle.authforge.UserWithToken; @@ -84,6 +84,7 @@ public User register(@RequestBody RegisterRequest request) throws CastleExceptio return userService.register(userRegisterRequest); } + @GetMapping("auth/verify") public UserWithToken verify(@RequestParam String loginIdentifier, @RequestParam String verificationCode) throws CastleException { userService.verify("email#" + loginIdentifier, verificationCode); diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/SpringBootExampleApplication.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/SpringBootExampleApplication.java index d2ada9d..dd4c668 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/SpringBootExampleApplication.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/SpringBootExampleApplication.java @@ -1,6 +1,6 @@ package org.clevercastle.authforge.examples.springboot.springbootexample; -import org.clevercastle.authforge.entity.User; +import org.clevercastle.authforge.model.User; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserLoginItemRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserLoginItemRepositoryAdapter.java index 10176e1..c0a3b17 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserLoginItemRepositoryAdapter.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserLoginItemRepositoryAdapter.java @@ -1,6 +1,6 @@ package org.clevercastle.authforge.examples.springboot.springbootexample; -import org.clevercastle.authforge.entity.UserLoginItem; +import org.clevercastle.authforge.model.UserLoginItem; import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserLoginItemRepository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserModelRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserModelRepositoryAdapter.java index f038048..6ee6bdf 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserModelRepositoryAdapter.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserModelRepositoryAdapter.java @@ -1,6 +1,6 @@ package org.clevercastle.authforge.examples.springboot.springbootexample; -import org.clevercastle.authforge.entity.User; +import org.clevercastle.authforge.model.User; import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserModelRepository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserRefreshTokenMappingRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserRefreshTokenMappingRepositoryAdapter.java index 2692ed9..259bba7 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserRefreshTokenMappingRepositoryAdapter.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserRefreshTokenMappingRepositoryAdapter.java @@ -1,6 +1,6 @@ package org.clevercastle.authforge.examples.springboot.springbootexample; -import org.clevercastle.authforge.entity.UserRefreshTokenMapping; +import org.clevercastle.authforge.model.UserRefreshTokenMapping; import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserRefreshTokenMappingRepository; import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserRefreshTokenMappingId; import org.springframework.data.jpa.repository.JpaRepository; From 2de443e03f6d3401e50a998acf79ba1516108565 Mon Sep 17 00:00:00 2001 From: ivyxjc Date: Tue, 13 May 2025 18:31:18 +0800 Subject: [PATCH 4/7] feat(): login in with one time password --- .../org/clevercastle/authforge/Config.java | 26 ++++--- .../clevercastle/authforge/UserService.java | 6 ++ .../authforge/UserServiceImpl.java | 74 +++++++++++++++++-- .../authforge/code/CodeSender.java | 9 +++ .../authforge/code/DummyCodeSender.java | 23 ++++++ .../SendCodeResponse.java} | 6 +- .../authforge/dto/OneTimePasswordDto.java | 33 +++++++++ .../authforge/model/OneTimePassword.java | 60 +++++++++++++++ .../authforge/repository/UserRepository.java | 5 ++ .../dynamodb/DynamodbUserRepositoryImpl.java | 14 +++- .../rdsjpa/RdsJpaOneTimePasswordId.java | 32 ++++++++ .../RdsJpaOneTimePasswordRepository.java | 13 ++++ .../rdsjpa/RdsJpaUserRepositoryImpl.java | 26 ++++++- .../clevercastle/authforge/util/CodeUtil.java | 27 +++---- .../AbstractVerificationService.java | 41 ---------- .../DummyVerificationService.java | 20 ----- .../verification/VerificationService.java | 9 --- 17 files changed, 317 insertions(+), 107 deletions(-) create mode 100644 src/main/java/org/clevercastle/authforge/code/CodeSender.java create mode 100644 src/main/java/org/clevercastle/authforge/code/DummyCodeSender.java rename src/main/java/org/clevercastle/authforge/{verification/SendVerificationCodeResponse.java => code/SendCodeResponse.java} (52%) create mode 100644 src/main/java/org/clevercastle/authforge/dto/OneTimePasswordDto.java create mode 100644 src/main/java/org/clevercastle/authforge/model/OneTimePassword.java create mode 100644 src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaOneTimePasswordId.java create mode 100644 src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaOneTimePasswordRepository.java delete mode 100644 src/main/java/org/clevercastle/authforge/verification/AbstractVerificationService.java delete mode 100644 src/main/java/org/clevercastle/authforge/verification/DummyVerificationService.java delete mode 100644 src/main/java/org/clevercastle/authforge/verification/VerificationService.java diff --git a/src/main/java/org/clevercastle/authforge/Config.java b/src/main/java/org/clevercastle/authforge/Config.java index 44e2822..c06ec4a 100644 --- a/src/main/java/org/clevercastle/authforge/Config.java +++ b/src/main/java/org/clevercastle/authforge/Config.java @@ -1,10 +1,12 @@ package org.clevercastle.authforge; public class Config { - // in seconds + // in second private int verificationCodeExpireTime; - // in seconds + // in second private int tokenExpireTime; + // in second + private int oneTimePasswordExpireTime; public int getVerificationCodeExpireTime() { return verificationCodeExpireTime; @@ -14,22 +16,22 @@ public int getTokenExpireTime() { return tokenExpireTime; } + public int getOneTimePasswordExpireTime() { + return oneTimePasswordExpireTime; + } public static ConfigBuilder builder() { return new ConfigBuilder(); } public static final class ConfigBuilder { - private int verificationCodeExpireTime = 300; - private int tokenExpireTime = 28400; + private int verificationCodeExpireTime; + private int tokenExpireTime; + private int oneTimePasswordExpireTime; private ConfigBuilder() { } - public static ConfigBuilder aConfig() { - return new ConfigBuilder(); - } - public ConfigBuilder verificationCodeExpireTime(int verificationCodeExpireTime) { this.verificationCodeExpireTime = verificationCodeExpireTime; return this; @@ -40,10 +42,16 @@ public ConfigBuilder tokenExpireTime(int tokenExpireTime) { return this; } + public ConfigBuilder oneTimePasswordExpireTime(int oneTimePasswordExpireTime) { + this.oneTimePasswordExpireTime = oneTimePasswordExpireTime; + return this; + } + public Config build() { Config config = new Config(); - config.verificationCodeExpireTime = this.verificationCodeExpireTime; config.tokenExpireTime = this.tokenExpireTime; + config.oneTimePasswordExpireTime = this.oneTimePasswordExpireTime; + config.verificationCodeExpireTime = this.verificationCodeExpireTime; return config; } } diff --git a/src/main/java/org/clevercastle/authforge/UserService.java b/src/main/java/org/clevercastle/authforge/UserService.java index b6276ad..6555474 100644 --- a/src/main/java/org/clevercastle/authforge/UserService.java +++ b/src/main/java/org/clevercastle/authforge/UserService.java @@ -1,9 +1,11 @@ package org.clevercastle.authforge; import org.apache.commons.lang3.tuple.Pair; +import org.clevercastle.authforge.dto.OneTimePasswordDto; import org.clevercastle.authforge.model.User; import org.clevercastle.authforge.model.UserLoginItem; import org.clevercastle.authforge.exception.CastleException; +import org.clevercastle.authforge.model.OneTimePassword; import org.clevercastle.authforge.oauth2.Oauth2ClientConfig; public interface UserService { @@ -24,4 +26,8 @@ public interface UserService { // used for sso login UserWithToken exchange(Oauth2ClientConfig clientConfig, String authorizationCode, String state, String redirectUrl) throws CastleException; + + OneTimePasswordDto requestOneTimePassword(String loginIdentifier) throws CastleException; + + UserWithToken verifyOneTimePassword(String loginIdentifier, String oneTimePassword) throws CastleException; } diff --git a/src/main/java/org/clevercastle/authforge/UserServiceImpl.java b/src/main/java/org/clevercastle/authforge/UserServiceImpl.java index b9a79d6..51943b7 100644 --- a/src/main/java/org/clevercastle/authforge/UserServiceImpl.java +++ b/src/main/java/org/clevercastle/authforge/UserServiceImpl.java @@ -14,9 +14,11 @@ import jakarta.annotation.Nonnull; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; +import org.clevercastle.authforge.dto.OneTimePasswordDto; import org.clevercastle.authforge.exception.CastleException; import org.clevercastle.authforge.exception.UserExistException; import org.clevercastle.authforge.exception.UserNotFoundException; +import org.clevercastle.authforge.model.OneTimePassword; import org.clevercastle.authforge.model.User; import org.clevercastle.authforge.model.UserLoginItem; import org.clevercastle.authforge.oauth2.Oauth2ClientConfig; @@ -27,7 +29,7 @@ import org.clevercastle.authforge.util.HashUtil; import org.clevercastle.authforge.util.IdUtil; import org.clevercastle.authforge.util.TimeUtils; -import org.clevercastle.authforge.verification.VerificationService; +import org.clevercastle.authforge.code.CodeSender; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,16 +46,16 @@ public class UserServiceImpl implements UserService { private final Config config; private final UserRepository userRepository; private final TokenService tokenService; - private final VerificationService verificationService; + private final CodeSender codeSender; public UserServiceImpl(Config config, UserRepository userRepository, TokenService tokenService, - VerificationService verificationService) { + CodeSender codeSender) { this.config = config; this.userRepository = userRepository; this.tokenService = tokenService; - this.verificationService = verificationService; + this.codeSender = codeSender; } @Override @@ -90,7 +92,7 @@ public User register(UserRegisterRequest userRegisterRequest) throws CastleExcep userLoginItem.setCreatedAt(now); userLoginItem.setUpdatedAt(now); this.userRepository.save(user, userLoginItem); - this.verificationService.sendVerificationCode(userLoginItem.getLoginIdentifier(), userLoginItem.getVerificationCode()); + this.codeSender.sendVerificationCode(userLoginItem.getLoginIdentifier(), userLoginItem.getVerificationCode()); return user; } @@ -108,7 +110,18 @@ public void verify(String loginIdentifier, String verificationCode) throws Castl if (!StringUtils.equals(verificationCode, userLoginItem.getVerificationCode())) { throw new CastleException(); } - this.verificationService.verify(loginIdentifier, verificationCode); + 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 @@ -274,4 +287,53 @@ public Pair getByLoginIdentifier(String loginIdentifier) th 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); + } } diff --git a/src/main/java/org/clevercastle/authforge/code/CodeSender.java b/src/main/java/org/clevercastle/authforge/code/CodeSender.java new file mode 100644 index 0000000..f7df037 --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/code/CodeSender.java @@ -0,0 +1,9 @@ +package org.clevercastle.authforge.code; + +import org.clevercastle.authforge.exception.CastleException; + +public interface CodeSender { + void sendVerificationCode(String loginIdentifier, String verificationCode) throws CastleException; + + void sendOneTimePassword(String loginIdentifier, String oneTimePasswordService) throws CastleException; +} diff --git a/src/main/java/org/clevercastle/authforge/code/DummyCodeSender.java b/src/main/java/org/clevercastle/authforge/code/DummyCodeSender.java new file mode 100644 index 0000000..b9302bc --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/code/DummyCodeSender.java @@ -0,0 +1,23 @@ +package org.clevercastle.authforge.code; + +import org.clevercastle.authforge.exception.CastleException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DummyCodeSender implements CodeSender { + private final Logger logger = LoggerFactory.getLogger(DummyCodeSender.class); + + public DummyCodeSender() { + } + + @Override + public void sendVerificationCode(String loginIdentifier, String verificationCode) throws CastleException { + logger.info("verification code is: {}", verificationCode); + + } + + @Override + public void sendOneTimePassword(String loginIdentifier, String verificationCode) throws CastleException { + logger.info("one time password is: {}", verificationCode); + } +} diff --git a/src/main/java/org/clevercastle/authforge/verification/SendVerificationCodeResponse.java b/src/main/java/org/clevercastle/authforge/code/SendCodeResponse.java similarity index 52% rename from src/main/java/org/clevercastle/authforge/verification/SendVerificationCodeResponse.java rename to src/main/java/org/clevercastle/authforge/code/SendCodeResponse.java index e070c68..dd99109 100644 --- a/src/main/java/org/clevercastle/authforge/verification/SendVerificationCodeResponse.java +++ b/src/main/java/org/clevercastle/authforge/code/SendCodeResponse.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.verification; +package org.clevercastle.authforge.code; -public class SendVerificationCodeResponse { +public class SendCodeResponse { public enum Type { success, error @@ -9,7 +9,7 @@ public enum Type { public Type type; public String message; - public SendVerificationCodeResponse(Type type, String message) { + public SendCodeResponse(Type type, String message) { this.type = type; this.message = message; } diff --git a/src/main/java/org/clevercastle/authforge/dto/OneTimePasswordDto.java b/src/main/java/org/clevercastle/authforge/dto/OneTimePasswordDto.java new file mode 100644 index 0000000..e8f6e0d --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/dto/OneTimePasswordDto.java @@ -0,0 +1,33 @@ +package org.clevercastle.authforge.dto; + +import java.time.OffsetDateTime; + +public class OneTimePasswordDto { + private String loginIdentifier; + private OffsetDateTime expiredAt; + private OffsetDateTime createdAt; + + public String getLoginIdentifier() { + return loginIdentifier; + } + + public void setLoginIdentifier(String loginIdentifier) { + this.loginIdentifier = loginIdentifier; + } + + 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; + } +} diff --git a/src/main/java/org/clevercastle/authforge/model/OneTimePassword.java b/src/main/java/org/clevercastle/authforge/model/OneTimePassword.java new file mode 100644 index 0000000..ef96e24 --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/model/OneTimePassword.java @@ -0,0 +1,60 @@ +package org.clevercastle.authforge.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; +import org.clevercastle.authforge.repository.rdsjpa.RdsJpaOneTimePasswordId; +import org.clevercastle.authforge.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; + private OffsetDateTime createdAt; + + 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; + } +} diff --git a/src/main/java/org/clevercastle/authforge/repository/UserRepository.java b/src/main/java/org/clevercastle/authforge/repository/UserRepository.java index 27c680b..f38cbc5 100644 --- a/src/main/java/org/clevercastle/authforge/repository/UserRepository.java +++ b/src/main/java/org/clevercastle/authforge/repository/UserRepository.java @@ -4,6 +4,7 @@ import org.apache.commons.lang3.tuple.Pair; import org.clevercastle.authforge.model.User; import org.clevercastle.authforge.model.UserLoginItem; +import org.clevercastle.authforge.model.OneTimePassword; import org.clevercastle.authforge.model.UserRefreshTokenMapping; import org.clevercastle.authforge.exception.CastleException; @@ -23,4 +24,8 @@ public interface UserRepository { 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; } \ No newline at end of file diff --git a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java b/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java index 8c6a551..32f2b1b 100644 --- a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java +++ b/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java @@ -1,12 +1,14 @@ package org.clevercastle.authforge.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.exception.CastleException; +import org.clevercastle.authforge.model.OneTimePassword; import org.clevercastle.authforge.model.User; import org.clevercastle.authforge.model.UserLoginItem; import org.clevercastle.authforge.model.UserRefreshTokenMapping; -import org.clevercastle.authforge.exception.CastleException; import org.clevercastle.authforge.repository.UserRepository; import org.clevercastle.authforge.util.TimeUtils; import org.slf4j.Logger; @@ -163,4 +165,14 @@ private DynamoDbTable getTable() { } 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(); + } } diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaOneTimePasswordId.java b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaOneTimePasswordId.java new file mode 100644 index 0000000..dc15b1d --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaOneTimePasswordId.java @@ -0,0 +1,32 @@ +package org.clevercastle.authforge.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/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaOneTimePasswordRepository.java b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaOneTimePasswordRepository.java new file mode 100644 index 0000000..8d7cf30 --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaOneTimePasswordRepository.java @@ -0,0 +1,13 @@ +package org.clevercastle.authforge.repository.rdsjpa; + +import org.clevercastle.authforge.model.OneTimePassword; + +import java.util.List; + +public interface RdsJpaOneTimePasswordRepository { + OneTimePassword save(OneTimePassword oneTimePassword); + + void deleteByLoginIdentifier(String loginIdentifier); + + List getByLoginIdentifier(String loginIdentifier); +} diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRepositoryImpl.java b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRepositoryImpl.java index 9ae1d22..45b3d4d 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRepositoryImpl.java +++ b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRepositoryImpl.java @@ -6,23 +6,28 @@ import org.clevercastle.authforge.exception.CastleException; import org.clevercastle.authforge.model.User; import org.clevercastle.authforge.model.UserLoginItem; +import org.clevercastle.authforge.model.OneTimePassword; import org.clevercastle.authforge.model.UserRefreshTokenMapping; import org.clevercastle.authforge.repository.UserRepository; import org.clevercastle.authforge.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; public RdsJpaUserRepositoryImpl(RdsJpaUserModelRepository userModelRepository, RdsJpaUserLoginItemRepository userLoginItemRepository, - RdsJpaUserRefreshTokenMappingRepository userRefreshTokenMappingRepository) { + RdsJpaUserRefreshTokenMappingRepository userRefreshTokenMappingRepository, + RdsJpaOneTimePasswordRepository oneTimePasswordRepository) { this.userModelRepository = userModelRepository; this.userLoginItemRepository = userLoginItemRepository; this.userRefreshTokenMappingRepository = userRefreshTokenMappingRepository; + this.oneTimePasswordRepository = oneTimePasswordRepository; } @Override @@ -83,4 +88,23 @@ public Pair getByUserSub(String userSub) { } 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; + } } diff --git a/src/main/java/org/clevercastle/authforge/util/CodeUtil.java b/src/main/java/org/clevercastle/authforge/util/CodeUtil.java index 3720c1d..3c51ddc 100644 --- a/src/main/java/org/clevercastle/authforge/util/CodeUtil.java +++ b/src/main/java/org/clevercastle/authforge/util/CodeUtil.java @@ -3,30 +3,23 @@ import java.util.Random; public class CodeUtil { - private static final String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - private static final String REFERRAL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"; + public static final String UPPER_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + public static final String FULL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"; private static final Random RANDOM = new Random(); - private CodeUtil(){} - - public static String generateCode(int length) { - StringBuilder code = new StringBuilder(); - for (int i = 0; i < length; i++) { - int index = RANDOM.nextInt(CHARS.length()); - code.append(CHARS.charAt(index)); - } - return code.toString(); + private CodeUtil() { } - - public static String generateReferralCode(int length) { + public static String generateCode(int length, String chars) { StringBuilder code = new StringBuilder(); for (int i = 0; i < length; i++) { - int index = RANDOM.nextInt(REFERRAL_CHARS.length()); - code.append(REFERRAL_CHARS.charAt(index)); + int index = RANDOM.nextInt(chars.length()); + code.append(chars.charAt(index)); } return code.toString(); } - -} + public static String generateCode(int length) { + return generateCode(length, FULL_CHARS); + } +} \ No newline at end of file diff --git a/src/main/java/org/clevercastle/authforge/verification/AbstractVerificationService.java b/src/main/java/org/clevercastle/authforge/verification/AbstractVerificationService.java deleted file mode 100644 index 5328990..0000000 --- a/src/main/java/org/clevercastle/authforge/verification/AbstractVerificationService.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.clevercastle.authforge.verification; - -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.clevercastle.authforge.exception.CastleException; -import org.clevercastle.authforge.model.User; -import org.clevercastle.authforge.model.UserLoginItem; -import org.clevercastle.authforge.repository.UserRepository; -import org.clevercastle.authforge.util.TimeUtils; - -public abstract class AbstractVerificationService implements VerificationService { - private final UserRepository userRepository; - - protected AbstractVerificationService(UserRepository userRepository) { - this.userRepository = userRepository; - } - - @Override - public void verify(String loginIdentifier, String verificationCode) throws CastleException { - if (StringUtils.isBlank(verificationCode)) { - throw new CastleException(); - } - Pair pair = userRepository.getByLoginIdentifier(loginIdentifier); - UserLoginItem userLoginItem = pair.getRight(); - if (userLoginItem == null) { - 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); - } - } -} diff --git a/src/main/java/org/clevercastle/authforge/verification/DummyVerificationService.java b/src/main/java/org/clevercastle/authforge/verification/DummyVerificationService.java deleted file mode 100644 index 2f12575..0000000 --- a/src/main/java/org/clevercastle/authforge/verification/DummyVerificationService.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.clevercastle.authforge.verification; - -import org.clevercastle.authforge.exception.CastleException; -import org.clevercastle.authforge.repository.UserRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class DummyVerificationService extends AbstractVerificationService { - private final Logger logger = LoggerFactory.getLogger(DummyVerificationService.class); - - public DummyVerificationService(UserRepository userRepository) { - super(userRepository); - } - - @Override - public void sendVerificationCode(String loginIdentifier, String verificationCode) throws CastleException { - logger.info("verification code is: {}", verificationCode); - - } -} diff --git a/src/main/java/org/clevercastle/authforge/verification/VerificationService.java b/src/main/java/org/clevercastle/authforge/verification/VerificationService.java deleted file mode 100644 index 01ba06c..0000000 --- a/src/main/java/org/clevercastle/authforge/verification/VerificationService.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.clevercastle.authforge.verification; - -import org.clevercastle.authforge.exception.CastleException; - -public interface VerificationService { - void sendVerificationCode(String loginIdentifier, String verificationCode) throws CastleException; - - void verify(String loginIdentifier, String verificationCode) throws CastleException; -} From be0d2e160ab96bbc1869981639aefe7244971128 Mon Sep 17 00:00:00 2001 From: ivyxjc Date: Tue, 13 May 2025 18:31:50 +0800 Subject: [PATCH 5/7] feat(example): login with one time password --- .../springbootexample/AuthController.java | 16 ++++++++++++-- .../springboot/springbootexample/Beans.java | 14 +++++++----- .../OneTimePasswordRepositoryAdapter.java | 9 ++++++++ .../springbootexample/SendOneTimeRequest.java | 13 +++++++++++ .../VerifyOneTimeRequest.java | 22 +++++++++++++++++++ 5 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/OneTimePasswordRepositoryAdapter.java create mode 100644 examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/SendOneTimeRequest.java create mode 100644 examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/VerifyOneTimeRequest.java diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/AuthController.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/AuthController.java index 3c51a83..2a7ae46 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/AuthController.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/AuthController.java @@ -4,7 +4,9 @@ import com.auth0.jwt.interfaces.DecodedJWT; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; +import org.clevercastle.authforge.dto.OneTimePasswordDto; import org.clevercastle.authforge.exception.CastleException; +import org.clevercastle.authforge.model.OneTimePassword; import org.clevercastle.authforge.model.User; import org.clevercastle.authforge.model.UserLoginItem; import org.clevercastle.authforge.UserRegisterRequest; @@ -86,8 +88,8 @@ public User register(@RequestBody RegisterRequest request) throws CastleExceptio @GetMapping("auth/verify") - public UserWithToken verify(@RequestParam String loginIdentifier, @RequestParam String verificationCode) throws CastleException { - userService.verify("email#" + loginIdentifier, verificationCode); + public UserWithToken verify(@RequestParam String email, @RequestParam String verificationCode) throws CastleException { + userService.verify("email#" + email, verificationCode); return null; } @@ -141,4 +143,14 @@ public UserWithToken exchange(@RequestParam SsoType ssoType, @RequestParam Strin } return null; } + + @GetMapping("auth/one-time-password") + public OneTimePasswordDto requestOneTimePassword(@RequestParam String email) throws CastleException { + return userService.requestOneTimePassword("email#" + email); + } + + @PostMapping("auth/one-time-password") + public UserWithToken verifyOneTimePassword(@RequestBody VerifyOneTimeRequest request) throws CastleException { + return userService.verifyOneTimePassword("email#" + request.getEmail(), request.getOneTimePassword()); + } } diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/Beans.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/Beans.java index 5aa8f63..35d71b9 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/Beans.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/Beans.java @@ -5,15 +5,15 @@ import org.clevercastle.authforge.UserService; import org.clevercastle.authforge.UserServiceImpl; import org.clevercastle.authforge.repository.UserRepository; -import org.clevercastle.authforge.repository.dynamodb.DynamodbUser; import org.clevercastle.authforge.repository.dynamodb.DynamodbUserRepositoryImpl; +import org.clevercastle.authforge.repository.rdsjpa.RdsJpaOneTimePasswordRepository; import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserLoginItemRepository; import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserModelRepository; import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserRefreshTokenMappingRepository; import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserRepositoryImpl; import org.clevercastle.authforge.token.TokenService; import org.clevercastle.authforge.token.jwt.JwtTokenService; -import org.clevercastle.authforge.verification.DummyVerificationService; +import org.clevercastle.authforge.code.DummyCodeSender; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; @@ -37,8 +37,10 @@ public class Beans { @Bean public UserRepository userRepository(RdsJpaUserModelRepository userModelRepository, RdsJpaUserLoginItemRepository userLoginItemRepository, - RdsJpaUserRefreshTokenMappingRepository userRefreshTokenMappingRepository) { - return new RdsJpaUserRepositoryImpl(userModelRepository, userLoginItemRepository, userRefreshTokenMappingRepository); + RdsJpaUserRefreshTokenMappingRepository userRefreshTokenMappingRepository, + RdsJpaOneTimePasswordRepository oneTimePasswordRepository) { + return new RdsJpaUserRepositoryImpl(userModelRepository, userLoginItemRepository, + userRefreshTokenMappingRepository, oneTimePasswordRepository); } @@ -77,8 +79,8 @@ public TokenService tokenService() throws NoSuchAlgorithmException, InvalidKeySp } @Bean - public UserService userService(UserRepository dynamodbUserRepository, TokenService tokenService) { - return new UserServiceImpl(Config.builder().build(), dynamodbUserRepository, tokenService, new DummyVerificationService(dynamodbUserRepository)); + public UserService userService(UserRepository userRepository, TokenService tokenService) { + return new UserServiceImpl(Config.builder().build(), userRepository, tokenService, new DummyCodeSender()); } diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/OneTimePasswordRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/OneTimePasswordRepositoryAdapter.java new file mode 100644 index 0000000..e180bbd --- /dev/null +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/OneTimePasswordRepositoryAdapter.java @@ -0,0 +1,9 @@ +package org.clevercastle.authforge.examples.springboot.springbootexample; + +import org.clevercastle.authforge.model.OneTimePassword; +import org.clevercastle.authforge.repository.rdsjpa.RdsJpaOneTimePasswordId; +import org.clevercastle.authforge.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/examples/springboot/springbootexample/SendOneTimeRequest.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/SendOneTimeRequest.java new file mode 100644 index 0000000..b678839 --- /dev/null +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/SendOneTimeRequest.java @@ -0,0 +1,13 @@ +package org.clevercastle.authforge.examples.springboot.springbootexample; + +public class SendOneTimeRequest { + private String email; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/VerifyOneTimeRequest.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/VerifyOneTimeRequest.java new file mode 100644 index 0000000..267247d --- /dev/null +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/VerifyOneTimeRequest.java @@ -0,0 +1,22 @@ +package org.clevercastle.authforge.examples.springboot.springbootexample; + +public class VerifyOneTimeRequest { + private String email; + private String oneTimePassword; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getOneTimePassword() { + return oneTimePassword; + } + + public void setOneTimePassword(String oneTimePassword) { + this.oneTimePassword = oneTimePassword; + } +} From 689604c294ccc538a5ee72db0df3af268a6f4e9a Mon Sep 17 00:00:00 2001 From: ivyxjc Date: Wed, 14 May 2025 10:48:38 +0800 Subject: [PATCH 6/7] feat(): setup mfa --- docs/mfa.md | 18 +++++ .../clevercastle/authforge/CacheService.java | 9 +++ .../authforge/DummyCacheServiceImpl.java | 26 ++++++ .../clevercastle/authforge/UserService.java | 15 +++- .../authforge/UserServiceImpl.java | 52 +++++++++++- .../authforge/model/ChallengeSession.java | 57 +++++++++++++ .../authforge/model/ResourceType.java | 7 ++ .../authforge/model/UserHmacSecret.java | 79 +++++++++++++++++++ .../authforge/repository/UserRepository.java | 13 ++- .../dynamodb/DynamodbUserRepositoryImpl.java | 17 ++++ .../RdsJpaChallengeSessionRepository.java | 9 +++ .../rdsjpa/RdsJpaUserHmacSecretId.java | 24 ++++++ .../RdsJpaUserHmacSecretRepository.java | 11 +++ .../rdsjpa/RdsJpaUserRepositoryImpl.java | 25 +++++- .../authforge/totp/RequestTotpResponse.java | 22 ++++++ .../authforge/totp/SetupTotpRequest.java | 33 ++++++++ .../totp/SetupTotpVerificationCode.java | 24 ++++++ .../clevercastle/authforge/util/IdUtil.java | 6 ++ 18 files changed, 440 insertions(+), 7 deletions(-) create mode 100644 docs/mfa.md create mode 100644 src/main/java/org/clevercastle/authforge/CacheService.java create mode 100644 src/main/java/org/clevercastle/authforge/DummyCacheServiceImpl.java create mode 100644 src/main/java/org/clevercastle/authforge/model/ChallengeSession.java create mode 100644 src/main/java/org/clevercastle/authforge/model/ResourceType.java create mode 100644 src/main/java/org/clevercastle/authforge/model/UserHmacSecret.java create mode 100644 src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaChallengeSessionRepository.java create mode 100644 src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserHmacSecretId.java create mode 100644 src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserHmacSecretRepository.java create mode 100644 src/main/java/org/clevercastle/authforge/totp/RequestTotpResponse.java create mode 100644 src/main/java/org/clevercastle/authforge/totp/SetupTotpRequest.java create mode 100644 src/main/java/org/clevercastle/authforge/totp/SetupTotpVerificationCode.java diff --git a/docs/mfa.md b/docs/mfa.md new file mode 100644 index 0000000..f602307 --- /dev/null +++ b/docs/mfa.md @@ -0,0 +1,18 @@ +# Mfa Flow + +```mermaid +flowchart TD + A[User login] --> B{Authenticated?} + B -- No --> Z[Fail to login] + B -- Yes --> C[Show the mfa setup options] + C --> D[User: Setup mfa] + D --> E[Server generate the secret key and return the QR code string and one session-id, save this secret key in the system with key session-id] + E --> F[Client: Show the QR code to the user,] + F --> G[User: user use authenticator app to scan the QR code] + G --> H[User: user input the first verification code] + H --> I[Client: pass the session-id and the verification code to the server] + I --> J[Server: get the secret key based on the session-id and verify the code] + J -- Success to verify --> K[Server: Notify the user to input the verification code again] + J -- Fail to verify --> L[Server: Save the secret key to the database and return success] + K --> I +``` \ No newline at end of file diff --git a/src/main/java/org/clevercastle/authforge/CacheService.java b/src/main/java/org/clevercastle/authforge/CacheService.java new file mode 100644 index 0000000..0a3f8ba --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/CacheService.java @@ -0,0 +1,9 @@ +package org.clevercastle.authforge; + +public interface CacheService { + void set(String key, String value, long ttl); + + String get(String key); + + boolean delete(String key); +} diff --git a/src/main/java/org/clevercastle/authforge/DummyCacheServiceImpl.java b/src/main/java/org/clevercastle/authforge/DummyCacheServiceImpl.java new file mode 100644 index 0000000..54ad2fd --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/DummyCacheServiceImpl.java @@ -0,0 +1,26 @@ +package org.clevercastle.authforge; + +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +public class DummyCacheServiceImpl implements CacheService { + Map map = new HashMap<>(); + + @Override + public void set(String key, String value, long ttl) { + map.put(key, value); + } + + @Override + public String get(String key) { + return map.get(key); + } + + @Override + public boolean delete(String key) { + String value = map.remove(key); + return StringUtils.isNotBlank(value); + } +} diff --git a/src/main/java/org/clevercastle/authforge/UserService.java b/src/main/java/org/clevercastle/authforge/UserService.java index 6555474..5df10ab 100644 --- a/src/main/java/org/clevercastle/authforge/UserService.java +++ b/src/main/java/org/clevercastle/authforge/UserService.java @@ -2,11 +2,13 @@ import org.apache.commons.lang3.tuple.Pair; import org.clevercastle.authforge.dto.OneTimePasswordDto; +import org.clevercastle.authforge.exception.CastleException; +import org.clevercastle.authforge.model.ChallengeSession; import org.clevercastle.authforge.model.User; import org.clevercastle.authforge.model.UserLoginItem; -import org.clevercastle.authforge.exception.CastleException; -import org.clevercastle.authforge.model.OneTimePassword; import org.clevercastle.authforge.oauth2.Oauth2ClientConfig; +import org.clevercastle.authforge.totp.RequestTotpResponse; +import org.clevercastle.authforge.totp.SetupTotpRequest; public interface UserService { // used for username/password, email/password, mobile/password @@ -27,7 +29,16 @@ public interface UserService { // 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; } diff --git a/src/main/java/org/clevercastle/authforge/UserServiceImpl.java b/src/main/java/org/clevercastle/authforge/UserServiceImpl.java index 51943b7..a142cad 100644 --- a/src/main/java/org/clevercastle/authforge/UserServiceImpl.java +++ b/src/main/java/org/clevercastle/authforge/UserServiceImpl.java @@ -14,22 +14,27 @@ import jakarta.annotation.Nonnull; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; +import org.clevercastle.authforge.code.CodeSender; import org.clevercastle.authforge.dto.OneTimePasswordDto; import org.clevercastle.authforge.exception.CastleException; import org.clevercastle.authforge.exception.UserExistException; import org.clevercastle.authforge.exception.UserNotFoundException; +import org.clevercastle.authforge.model.ChallengeSession; import org.clevercastle.authforge.model.OneTimePassword; +import org.clevercastle.authforge.model.ResourceType; import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.model.UserHmacSecret; import org.clevercastle.authforge.model.UserLoginItem; import org.clevercastle.authforge.oauth2.Oauth2ClientConfig; import org.clevercastle.authforge.oauth2.Oauth2User; import org.clevercastle.authforge.repository.UserRepository; import org.clevercastle.authforge.token.TokenService; +import org.clevercastle.authforge.totp.RequestTotpResponse; +import org.clevercastle.authforge.totp.SetupTotpRequest; import org.clevercastle.authforge.util.CodeUtil; import org.clevercastle.authforge.util.HashUtil; import org.clevercastle.authforge.util.IdUtil; import org.clevercastle.authforge.util.TimeUtils; -import org.clevercastle.authforge.code.CodeSender; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,15 +52,18 @@ public class UserServiceImpl implements UserService { 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) { + CodeSender codeSender, + CacheService cacheService) { this.config = config; this.userRepository = userRepository; this.tokenService = tokenService; this.codeSender = codeSender; + this.cacheService = cacheService; } @Override @@ -336,4 +344,44 @@ public UserWithToken verifyOneTimePassword(String loginIdentifier, String oneTim 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 key = UUID.randomUUID().toString(); + String secret = UUID.randomUUID().toString(); + cacheService.set(key, secret, 120); + RequestTotpResponse requestTotpDto = new RequestTotpResponse(); + requestTotpDto.setSessionId(key); + requestTotpDto.setSecret(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(); + } + // todo verify the user input verification code + 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(request.getName()); + userRepository.createHmacSecret(userHmacSecret); + } } diff --git a/src/main/java/org/clevercastle/authforge/model/ChallengeSession.java b/src/main/java/org/clevercastle/authforge/model/ChallengeSession.java new file mode 100644 index 0000000..18f3bd2 --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/model/ChallengeSession.java @@ -0,0 +1,57 @@ +package org.clevercastle.authforge.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, + } + + @javax.persistence.Id + @Id + private String id; + private Type type; + + private String userId; + private OffsetDateTime createdAt; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/src/main/java/org/clevercastle/authforge/model/ResourceType.java b/src/main/java/org/clevercastle/authforge/model/ResourceType.java new file mode 100644 index 0000000..1d56821 --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/model/ResourceType.java @@ -0,0 +1,7 @@ +package org.clevercastle.authforge.model; + +public enum ResourceType { + user, + challengeSession, + totp +} diff --git a/src/main/java/org/clevercastle/authforge/model/UserHmacSecret.java b/src/main/java/org/clevercastle/authforge/model/UserHmacSecret.java new file mode 100644 index 0000000..2a638c2 --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/model/UserHmacSecret.java @@ -0,0 +1,79 @@ +package org.clevercastle.authforge.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; +import org.clevercastle.authforge.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; + private String name; + + private OffsetDateTime lastUsedAt; + + private OffsetDateTime createdAt; + + 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; + } +} diff --git a/src/main/java/org/clevercastle/authforge/repository/UserRepository.java b/src/main/java/org/clevercastle/authforge/repository/UserRepository.java index f38cbc5..b66e36c 100644 --- a/src/main/java/org/clevercastle/authforge/repository/UserRepository.java +++ b/src/main/java/org/clevercastle/authforge/repository/UserRepository.java @@ -2,13 +2,16 @@ import jakarta.annotation.Nonnull; import org.apache.commons.lang3.tuple.Pair; +import org.clevercastle.authforge.exception.CastleException; +import org.clevercastle.authforge.model.ChallengeSession; +import org.clevercastle.authforge.model.OneTimePassword; import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.model.UserHmacSecret; import org.clevercastle.authforge.model.UserLoginItem; -import org.clevercastle.authforge.model.OneTimePassword; import org.clevercastle.authforge.model.UserRefreshTokenMapping; -import org.clevercastle.authforge.exception.CastleException; import java.time.OffsetDateTime; +import java.util.List; public interface UserRepository { void save(User user, UserLoginItem userLoginItem) throws CastleException; @@ -28,4 +31,10 @@ public interface UserRepository { void saveOneTimePassword(OneTimePassword userOneTimePasswordMapping) throws CastleException; boolean verifyOneTimePassword(String loginIdentifier, String oneTimePassword) throws CastleException; + + void createHmacSecret(UserHmacSecret userHmacSecret) throws CastleException; + + List listHmacSecretByUserId(String userId) throws CastleException; + + void createChallenge(ChallengeSession session) throws CastleException; } \ No newline at end of file diff --git a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java b/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java index 32f2b1b..119e46f 100644 --- a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java +++ b/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java @@ -5,8 +5,10 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.clevercastle.authforge.exception.CastleException; +import org.clevercastle.authforge.model.ChallengeSession; import org.clevercastle.authforge.model.OneTimePassword; import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.model.UserHmacSecret; import org.clevercastle.authforge.model.UserLoginItem; import org.clevercastle.authforge.model.UserRefreshTokenMapping; import org.clevercastle.authforge.repository.UserRepository; @@ -175,4 +177,19 @@ public void saveOneTimePassword(OneTimePassword userOneTimePasswordMapping) thro 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/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaChallengeSessionRepository.java b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaChallengeSessionRepository.java new file mode 100644 index 0000000..f90d022 --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaChallengeSessionRepository.java @@ -0,0 +1,9 @@ +package org.clevercastle.authforge.repository.rdsjpa; + +import org.clevercastle.authforge.model.ChallengeSession; + +public interface RdsJpaChallengeSessionRepository { + ChallengeSession save(ChallengeSession challengeSession); + + ChallengeSession getById(String id); +} diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserHmacSecretId.java b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserHmacSecretId.java new file mode 100644 index 0000000..04a5128 --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserHmacSecretId.java @@ -0,0 +1,24 @@ +package org.clevercastle.authforge.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/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserHmacSecretRepository.java b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserHmacSecretRepository.java new file mode 100644 index 0000000..b561fa0 --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserHmacSecretRepository.java @@ -0,0 +1,11 @@ +package org.clevercastle.authforge.repository.rdsjpa; + +import org.clevercastle.authforge.model.UserHmacSecret; + +import java.util.List; + +public interface RdsJpaUserHmacSecretRepository { + UserHmacSecret save(UserHmacSecret userHmacSecret); + + List getByUserId(String userId); +} diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRepositoryImpl.java b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRepositoryImpl.java index 45b3d4d..eda5c8b 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRepositoryImpl.java +++ b/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRepositoryImpl.java @@ -4,7 +4,9 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.clevercastle.authforge.exception.CastleException; +import org.clevercastle.authforge.model.ChallengeSession; import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.model.UserHmacSecret; import org.clevercastle.authforge.model.UserLoginItem; import org.clevercastle.authforge.model.OneTimePassword; import org.clevercastle.authforge.model.UserRefreshTokenMapping; @@ -19,15 +21,21 @@ public class RdsJpaUserRepositoryImpl implements UserRepository { 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) { + 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 @@ -107,4 +115,19 @@ public boolean verifyOneTimePassword(String loginIdentifier, String oneTimePassw } 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/src/main/java/org/clevercastle/authforge/totp/RequestTotpResponse.java b/src/main/java/org/clevercastle/authforge/totp/RequestTotpResponse.java new file mode 100644 index 0000000..26032a0 --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/totp/RequestTotpResponse.java @@ -0,0 +1,22 @@ +package org.clevercastle.authforge.totp; + +public class RequestTotpResponse { + private String sessionId; + private String secret; + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } +} diff --git a/src/main/java/org/clevercastle/authforge/totp/SetupTotpRequest.java b/src/main/java/org/clevercastle/authforge/totp/SetupTotpRequest.java new file mode 100644 index 0000000..9a54383 --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/totp/SetupTotpRequest.java @@ -0,0 +1,33 @@ +package org.clevercastle.authforge.totp; + +import java.util.List; + +public class SetupTotpRequest { + private String sessionId; + private String name; + private List codes; + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getCodes() { + return codes; + } + + public void setCodes(List codes) { + this.codes = codes; + } +} diff --git a/src/main/java/org/clevercastle/authforge/totp/SetupTotpVerificationCode.java b/src/main/java/org/clevercastle/authforge/totp/SetupTotpVerificationCode.java new file mode 100644 index 0000000..575ac12 --- /dev/null +++ b/src/main/java/org/clevercastle/authforge/totp/SetupTotpVerificationCode.java @@ -0,0 +1,24 @@ +package org.clevercastle.authforge.totp; + +import java.time.OffsetDateTime; + +public class SetupTotpVerificationCode { + private OffsetDateTime inputTime; + private String code; + + public OffsetDateTime getInputTime() { + return inputTime; + } + + public void setInputTime(OffsetDateTime inputTime) { + this.inputTime = inputTime; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } +} diff --git a/src/main/java/org/clevercastle/authforge/util/IdUtil.java b/src/main/java/org/clevercastle/authforge/util/IdUtil.java index de12e12..f05bbe9 100644 --- a/src/main/java/org/clevercastle/authforge/util/IdUtil.java +++ b/src/main/java/org/clevercastle/authforge/util/IdUtil.java @@ -1,9 +1,15 @@ package org.clevercastle.authforge.util; +import org.clevercastle.authforge.model.ResourceType; + import java.util.UUID; public class IdUtil { public static String genUserId() { return "user-" + UUID.randomUUID(); } + + public static String genId(ResourceType resourceType) { + return UUID.randomUUID().toString(); + } } From 49926d2f066a1e0d30bb4a08c7231453db3fef21 Mon Sep 17 00:00:00 2001 From: ivyxjc Date: Wed, 14 May 2025 10:48:45 +0800 Subject: [PATCH 7/7] feat(example): setup mfa --- .../springbootexample/AuthController.java | 7 ++-- .../springboot/springbootexample/Beans.java | 11 +++++-- .../ChallengeSessionRepositoryAdapter.java | 8 +++++ .../springbootexample/UserController.java | 33 +++++++++++++++++++ .../UserHmacSecretRepositoryAdapter.java | 11 +++++++ 5 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/ChallengeSessionRepositoryAdapter.java create mode 100644 examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserController.java create mode 100644 examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserHmacSecretRepositoryAdapter.java diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/AuthController.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/AuthController.java index 2a7ae46..e813433 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/AuthController.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/AuthController.java @@ -4,14 +4,13 @@ import com.auth0.jwt.interfaces.DecodedJWT; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; +import org.clevercastle.authforge.UserRegisterRequest; +import org.clevercastle.authforge.UserService; +import org.clevercastle.authforge.UserWithToken; import org.clevercastle.authforge.dto.OneTimePasswordDto; import org.clevercastle.authforge.exception.CastleException; -import org.clevercastle.authforge.model.OneTimePassword; import org.clevercastle.authforge.model.User; import org.clevercastle.authforge.model.UserLoginItem; -import org.clevercastle.authforge.UserRegisterRequest; -import org.clevercastle.authforge.UserService; -import org.clevercastle.authforge.UserWithToken; import org.clevercastle.authforge.oauth2.Oauth2ClientConfig; import org.clevercastle.authforge.oauth2.github.GithubOauth2ExchangeService; import org.clevercastle.authforge.oauth2.oidc.OidcExchangeService; diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/Beans.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/Beans.java index 35d71b9..0c60204 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/Beans.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/Beans.java @@ -1,12 +1,15 @@ package org.clevercastle.authforge.examples.springboot.springbootexample; import com.auth0.jwt.algorithms.Algorithm; +import org.clevercastle.authforge.DummyCacheServiceImpl; import org.clevercastle.authforge.Config; import org.clevercastle.authforge.UserService; import org.clevercastle.authforge.UserServiceImpl; import org.clevercastle.authforge.repository.UserRepository; import org.clevercastle.authforge.repository.dynamodb.DynamodbUserRepositoryImpl; +import org.clevercastle.authforge.repository.rdsjpa.RdsJpaChallengeSessionRepository; import org.clevercastle.authforge.repository.rdsjpa.RdsJpaOneTimePasswordRepository; +import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserHmacSecretRepository; import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserLoginItemRepository; import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserModelRepository; import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserRefreshTokenMappingRepository; @@ -38,9 +41,11 @@ public class Beans { public UserRepository userRepository(RdsJpaUserModelRepository userModelRepository, RdsJpaUserLoginItemRepository userLoginItemRepository, RdsJpaUserRefreshTokenMappingRepository userRefreshTokenMappingRepository, - RdsJpaOneTimePasswordRepository oneTimePasswordRepository) { + RdsJpaOneTimePasswordRepository oneTimePasswordRepository, + RdsJpaChallengeSessionRepository challengeSessionRepository, + RdsJpaUserHmacSecretRepository userHmacSecretRepository) { return new RdsJpaUserRepositoryImpl(userModelRepository, userLoginItemRepository, - userRefreshTokenMappingRepository, oneTimePasswordRepository); + userRefreshTokenMappingRepository, oneTimePasswordRepository, challengeSessionRepository, userHmacSecretRepository); } @@ -80,7 +85,7 @@ public TokenService tokenService() throws NoSuchAlgorithmException, InvalidKeySp @Bean public UserService userService(UserRepository userRepository, TokenService tokenService) { - return new UserServiceImpl(Config.builder().build(), userRepository, tokenService, new DummyCodeSender()); + return new UserServiceImpl(Config.builder().build(), userRepository, tokenService, new DummyCodeSender(), new DummyCacheServiceImpl()); } diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/ChallengeSessionRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/ChallengeSessionRepositoryAdapter.java new file mode 100644 index 0000000..24da352 --- /dev/null +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/ChallengeSessionRepositoryAdapter.java @@ -0,0 +1,8 @@ +package org.clevercastle.authforge.examples.springboot.springbootexample; + +import org.clevercastle.authforge.model.ChallengeSession; +import org.clevercastle.authforge.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/examples/springboot/springbootexample/UserController.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserController.java new file mode 100644 index 0000000..eff85fb --- /dev/null +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserController.java @@ -0,0 +1,33 @@ +package org.clevercastle.authforge.examples.springboot.springbootexample; + +import org.clevercastle.authforge.UserService; +import org.clevercastle.authforge.exception.CastleException; +import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.totp.RequestTotpResponse; +import org.clevercastle.authforge.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/examples/springboot/springbootexample/UserHmacSecretRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserHmacSecretRepositoryAdapter.java new file mode 100644 index 0000000..41d2c7f --- /dev/null +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserHmacSecretRepositoryAdapter.java @@ -0,0 +1,11 @@ +package org.clevercastle.authforge.examples.springboot.springbootexample; + +import org.clevercastle.authforge.model.ChallengeSession; +import org.clevercastle.authforge.model.UserHmacSecret; +import org.clevercastle.authforge.repository.rdsjpa.RdsJpaChallengeSessionRepository; +import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserHmacSecretId; +import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserHmacSecretRepository; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserHmacSecretRepositoryAdapter extends RdsJpaUserHmacSecretRepository, JpaRepository { +}