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/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..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,12 +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.exception.CastleException; -import org.clevercastle.authforge.entity.User; -import org.clevercastle.authforge.entity.UserLoginItem; 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.User; +import org.clevercastle.authforge.model.UserLoginItem; import org.clevercastle.authforge.oauth2.Oauth2ClientConfig; import org.clevercastle.authforge.oauth2.github.GithubOauth2ExchangeService; import org.clevercastle.authforge.oauth2.oidc.OidcExchangeService; @@ -84,9 +85,10 @@ 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); + public UserWithToken verify(@RequestParam String email, @RequestParam String verificationCode) throws CastleException { + userService.verify("email#" + email, verificationCode); return null; } @@ -140,4 +142,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..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,19 +1,22 @@ 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.DynamodbUser; 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; 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 +40,12 @@ public class Beans { @Bean public UserRepository userRepository(RdsJpaUserModelRepository userModelRepository, RdsJpaUserLoginItemRepository userLoginItemRepository, - RdsJpaUserRefreshTokenMappingRepository userRefreshTokenMappingRepository) { - return new RdsJpaUserRepositoryImpl(userModelRepository, userLoginItemRepository, userRefreshTokenMappingRepository); + RdsJpaUserRefreshTokenMappingRepository userRefreshTokenMappingRepository, + RdsJpaOneTimePasswordRepository oneTimePasswordRepository, + RdsJpaChallengeSessionRepository challengeSessionRepository, + RdsJpaUserHmacSecretRepository userHmacSecretRepository) { + return new RdsJpaUserRepositoryImpl(userModelRepository, userLoginItemRepository, + userRefreshTokenMappingRepository, oneTimePasswordRepository, challengeSessionRepository, userHmacSecretRepository); } @@ -77,8 +84,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(), 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/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/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/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 { +} 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; 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; + } +} 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/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/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 c522827..5df10ab 100644 --- a/src/main/java/org/clevercastle/authforge/UserService.java +++ b/src/main/java/org/clevercastle/authforge/UserService.java @@ -1,10 +1,14 @@ 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.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.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 @@ -24,4 +28,17 @@ 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 22fc8ba..a142cad 100644 --- a/src/main/java/org/clevercastle/authforge/UserServiceImpl.java +++ b/src/main/java/org/clevercastle/authforge/UserServiceImpl.java @@ -14,20 +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.entity.User; -import org.clevercastle.authforge.entity.UserLoginItem; +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.verification.VerificationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,16 +51,19 @@ 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; + private final CacheService cacheService; public UserServiceImpl(Config config, UserRepository userRepository, TokenService tokenService, - VerificationService verificationService) { + CodeSender codeSender, + CacheService cacheService) { this.config = config; this.userRepository = userRepository; this.tokenService = tokenService; - this.verificationService = verificationService; + this.codeSender = codeSender; + this.cacheService = cacheService; } @Override @@ -90,7 +100,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 +118,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 @@ -152,7 +173,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); @@ -274,4 +295,93 @@ 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); + } + + @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/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/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/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/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/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/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/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/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..b66e36c 100644 --- a/src/main/java/org/clevercastle/authforge/repository/UserRepository.java +++ b/src/main/java/org/clevercastle/authforge/repository/UserRepository.java @@ -2,12 +2,16 @@ 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.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 java.time.OffsetDateTime; +import java.util.List; public interface UserRepository { void save(User user, UserLoginItem userLoginItem) throws CastleException; @@ -23,4 +27,14 @@ 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; + + 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/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..119e46f 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,16 @@ 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.entity.User; -import org.clevercastle.authforge.entity.UserLoginItem; -import org.clevercastle.authforge.entity.UserRefreshTokenMapping; 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; import org.clevercastle.authforge.util.TimeUtils; import org.slf4j.Logger; @@ -163,4 +167,29 @@ 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(); + } + + @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/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/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/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/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/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..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,25 +4,38 @@ 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.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; 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; + private final RdsJpaChallengeSessionRepository challengeSessionRepository; + private final RdsJpaUserHmacSecretRepository userHmacSecretRepository; public RdsJpaUserRepositoryImpl(RdsJpaUserModelRepository userModelRepository, RdsJpaUserLoginItemRepository userLoginItemRepository, - RdsJpaUserRefreshTokenMappingRepository userRefreshTokenMappingRepository) { + RdsJpaUserRefreshTokenMappingRepository userRefreshTokenMappingRepository, + RdsJpaOneTimePasswordRepository oneTimePasswordRepository, + RdsJpaChallengeSessionRepository challengeSessionRepository, + RdsJpaUserHmacSecretRepository userHmacSecretRepository) { this.userModelRepository = userModelRepository; this.userLoginItemRepository = userLoginItemRepository; this.userRefreshTokenMappingRepository = userRefreshTokenMappingRepository; + this.oneTimePasswordRepository = oneTimePasswordRepository; + this.challengeSessionRepository = challengeSessionRepository; + this.userHmacSecretRepository = userHmacSecretRepository; } @Override @@ -83,4 +96,38 @@ 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; + } + + @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/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/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/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/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(); + } } 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 38d21da..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.entity.User; -import org.clevercastle.authforge.entity.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; -}