diff --git a/build.gradle.kts b/build.gradle.kts index 8153b80..b7266c8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,82 +8,26 @@ plugins { group = "org.clevercastle" version = "0.0.1-SNAPSHOT" -repositories { - mavenCentral() -} - -dependencies { - implementation("com.fasterxml.jackson.core:jackson-databind:2.18.3") - implementation("com.fasterxml.jackson.core:jackson-core:2.18.3") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.3") - - implementation("jakarta.annotation:jakarta.annotation-api:3.0.0") - implementation("javax.persistence:javax.persistence-api:2.2") - implementation("jakarta.persistence:jakarta.persistence-api:3.2.0") - implementation("com.auth0:java-jwt:4.5.0") - - implementation("com.nimbusds:oauth2-oidc-sdk:11.23.1") - - implementation("software.amazon.awssdk:dynamodb:2.31.31") - implementation("software.amazon.awssdk:dynamodb-enhanced:2.31.31") - compileOnly("org.springframework.boot:spring-boot-starter-data-jpa:2.7.18") - - implementation("org.apache.commons:commons-lang3:3.17.0") - implementation("org.slf4j:slf4j-api:2.0.17") - - testImplementation(platform("org.junit:junit-bom:5.10.0")) - testImplementation("org.junit.jupiter:junit-jupiter") - testImplementation("org.mockito:mockito-core:5.17.0") - testImplementation("ch.qos.logback:logback-classic:1.5.18") -} - -tasks.jacocoTestReport { - reports { - xml.required = true - xml.outputLocation = layout.buildDirectory.file("jacoco/jacocoTestReport.xml") +subprojects { + repositories { + mavenCentral() + } + apply { + plugin("java") + plugin("jacoco") + plugin("com.vanniktech.maven.publish") + plugin("signing") } -} - -tasks.test { - useJUnitPlatform() - finalizedBy(tasks.jacocoTestReport) -} -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) + java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } -} -tasks.withType().configureEach { - options.release.set(11) + tasks.withType().configureEach { + options.release.set(11) + } } -mavenPublishing { - pom { - name.set("auth-forge") - description.set("A description of what my library does.") - inceptionYear.set("2025") - url.set("https://github.com/clevercastle/auth-forge/") - licenses { - license { - name.set("The Apache License, Version 2.0") - url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") - distribution.set("http://www.apache.org/licenses/LICENSE-2.0.txt") - } - } - developers { - developer { - id.set("ivyxjc") - name.set("ivyxjc") - url.set("https://github.com/ivyxjc/") - } - } - scm { - url.set("https://github.com/clevercastle/auth-forge/") - connection.set("scm:git:git://github.com/clevercastle/auth-forge.git") - developerConnection.set("scm:git:ssh://git@github.com/clevercastle/auth-forge.git") - } - } -} \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 0000000..e830cad --- /dev/null +++ b/core/build.gradle.kts @@ -0,0 +1,76 @@ +dependencies { + implementation("com.fasterxml.jackson.core:jackson-databind:2.18.3") + implementation("com.fasterxml.jackson.core:jackson-core:2.18.3") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.3") + + implementation("jakarta.annotation:jakarta.annotation-api:3.0.0") + implementation("javax.persistence:javax.persistence-api:2.2") + implementation("jakarta.persistence:jakarta.persistence-api:3.2.0") + implementation("com.auth0:java-jwt:4.5.0") + + implementation("com.nimbusds:oauth2-oidc-sdk:11.23.1") + + implementation("software.amazon.awssdk:dynamodb:2.31.31") + implementation("software.amazon.awssdk:dynamodb-enhanced:2.31.31") + + compileOnly("org.springframework.boot:spring-boot-starter-data-jpa:2.7.18") + + implementation("org.apache.commons:commons-lang3:3.18.0") + implementation("commons-codec:commons-codec:1.15") + implementation("org.slf4j:slf4j-api:2.0.17") + + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-core:5.17.0") + testImplementation("ch.qos.logback:logback-classic:1.5.18") +} + +tasks.jacocoTestReport { + reports { + xml.required = true + xml.outputLocation = layout.buildDirectory.file("jacoco/jacocoTestReport.xml") + } +} + +tasks.test { + useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +tasks.withType().configureEach { + options.release.set(11) +} + +mavenPublishing { + pom { + name.set("auth-forge") + description.set("A description of what my library does.") + inceptionYear.set("2025") + url.set("https://github.com/clevercastle/auth-forge/") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + distribution.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + developers { + developer { + id.set("ivyxjc") + name.set("ivyxjc") + url.set("https://github.com/ivyxjc/") + } + } + scm { + url.set("https://github.com/clevercastle/auth-forge/") + connection.set("scm:git:git://github.com/clevercastle/auth-forge.git") + developerConnection.set("scm:git:ssh://git@github.com/clevercastle/auth-forge.git") + } + } +} \ No newline at end of file diff --git a/src/main/java/org/clevercastle/authforge/CacheService.java b/core/src/main/java/org/clevercastle/authforge/core/CacheService.java similarity index 78% rename from src/main/java/org/clevercastle/authforge/CacheService.java rename to core/src/main/java/org/clevercastle/authforge/core/CacheService.java index 0a3f8ba..fa82b0c 100644 --- a/src/main/java/org/clevercastle/authforge/CacheService.java +++ b/core/src/main/java/org/clevercastle/authforge/core/CacheService.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge; +package org.clevercastle.authforge.core; public interface CacheService { void set(String key, String value, long ttl); diff --git a/src/main/java/org/clevercastle/authforge/Config.java b/core/src/main/java/org/clevercastle/authforge/core/Config.java similarity index 97% rename from src/main/java/org/clevercastle/authforge/Config.java rename to core/src/main/java/org/clevercastle/authforge/core/Config.java index c06ec4a..c71ba71 100644 --- a/src/main/java/org/clevercastle/authforge/Config.java +++ b/core/src/main/java/org/clevercastle/authforge/core/Config.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge; +package org.clevercastle.authforge.core; public class Config { // in second diff --git a/src/main/java/org/clevercastle/authforge/DummyCacheServiceImpl.java b/core/src/main/java/org/clevercastle/authforge/core/DummyCacheServiceImpl.java similarity index 93% rename from src/main/java/org/clevercastle/authforge/DummyCacheServiceImpl.java rename to core/src/main/java/org/clevercastle/authforge/core/DummyCacheServiceImpl.java index 54ad2fd..bca35da 100644 --- a/src/main/java/org/clevercastle/authforge/DummyCacheServiceImpl.java +++ b/core/src/main/java/org/clevercastle/authforge/core/DummyCacheServiceImpl.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge; +package org.clevercastle.authforge.core; import org.apache.commons.lang3.StringUtils; diff --git a/src/main/java/org/clevercastle/authforge/RefreshToken.java b/core/src/main/java/org/clevercastle/authforge/core/RefreshToken.java similarity index 94% rename from src/main/java/org/clevercastle/authforge/RefreshToken.java rename to core/src/main/java/org/clevercastle/authforge/core/RefreshToken.java index 9a0d505..f31f6dd 100644 --- a/src/main/java/org/clevercastle/authforge/RefreshToken.java +++ b/core/src/main/java/org/clevercastle/authforge/core/RefreshToken.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge; +package org.clevercastle.authforge.core; import jakarta.annotation.Nonnull; diff --git a/src/main/java/org/clevercastle/authforge/TokenHolder.java b/core/src/main/java/org/clevercastle/authforge/core/TokenHolder.java similarity index 97% rename from src/main/java/org/clevercastle/authforge/TokenHolder.java rename to core/src/main/java/org/clevercastle/authforge/core/TokenHolder.java index cf4ed0c..7436eb8 100644 --- a/src/main/java/org/clevercastle/authforge/TokenHolder.java +++ b/core/src/main/java/org/clevercastle/authforge/core/TokenHolder.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge; +package org.clevercastle.authforge.core; import java.time.OffsetDateTime; diff --git a/src/main/java/org/clevercastle/authforge/UserRegisterRequest.java b/core/src/main/java/org/clevercastle/authforge/core/UserRegisterRequest.java similarity index 94% rename from src/main/java/org/clevercastle/authforge/UserRegisterRequest.java rename to core/src/main/java/org/clevercastle/authforge/core/UserRegisterRequest.java index eb2520d..290f495 100644 --- a/src/main/java/org/clevercastle/authforge/UserRegisterRequest.java +++ b/core/src/main/java/org/clevercastle/authforge/core/UserRegisterRequest.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge; +package org.clevercastle.authforge.core; public class UserRegisterRequest { private String loginIdentifier; diff --git a/src/main/java/org/clevercastle/authforge/UserService.java b/core/src/main/java/org/clevercastle/authforge/core/UserService.java similarity index 54% rename from src/main/java/org/clevercastle/authforge/UserService.java rename to core/src/main/java/org/clevercastle/authforge/core/UserService.java index 5df10ab..a9f44ee 100644 --- a/src/main/java/org/clevercastle/authforge/UserService.java +++ b/core/src/main/java/org/clevercastle/authforge/core/UserService.java @@ -1,14 +1,18 @@ -package org.clevercastle.authforge; +package org.clevercastle.authforge.core; 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.oauth2.Oauth2ClientConfig; -import org.clevercastle.authforge.totp.RequestTotpResponse; -import org.clevercastle.authforge.totp.SetupTotpRequest; +import org.clevercastle.authforge.core.dto.OneTimePasswordDto; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.mfa.dto.MfaChallengeResponse; +import org.clevercastle.authforge.core.mfa.dto.MfaFactorResponse; +import org.clevercastle.authforge.core.model.ChallengeSession; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.oauth2.Oauth2ClientConfig; +import org.clevercastle.authforge.core.totp.RequestTotpResponse; +import org.clevercastle.authforge.core.totp.SetupTotpRequest; + +import java.util.List; public interface UserService { // used for username/password, email/password, mobile/password @@ -41,4 +45,15 @@ public interface UserService { // setup mfa void setupTotp(User user, SetupTotpRequest request) throws CastleException; + + // MFA challenge and verification methods + MfaChallengeResponse createMfaChallenge(User user, String challengeType, String factorId) throws CastleException; + + boolean verifyMfaChallenge(String challengeId, String code, String bindingCode) throws CastleException; + + List listMfaFactors(String userId) throws CastleException; + + void deleteMfaFactor(String userId, String factorId) throws CastleException; + + boolean verifyTotpCode(String userId, String code) throws CastleException; } diff --git a/src/main/java/org/clevercastle/authforge/UserServiceImpl.java b/core/src/main/java/org/clevercastle/authforge/core/UserServiceImpl.java similarity index 64% rename from src/main/java/org/clevercastle/authforge/UserServiceImpl.java rename to core/src/main/java/org/clevercastle/authforge/core/UserServiceImpl.java index a142cad..dec7e48 100644 --- a/src/main/java/org/clevercastle/authforge/UserServiceImpl.java +++ b/core/src/main/java/org/clevercastle/authforge/core/UserServiceImpl.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge; +package org.clevercastle.authforge.core; import com.nimbusds.oauth2.sdk.AuthorizationCode; import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; @@ -14,27 +14,30 @@ 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.core.code.CodeSender; +import org.clevercastle.authforge.core.dto.OneTimePasswordDto; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.exception.UserExistException; +import org.clevercastle.authforge.core.exception.UserNotFoundException; +import org.clevercastle.authforge.core.mfa.dto.MfaChallengeResponse; +import org.clevercastle.authforge.core.mfa.dto.MfaFactorResponse; +import org.clevercastle.authforge.core.model.ChallengeSession; +import org.clevercastle.authforge.core.model.OneTimePassword; +import org.clevercastle.authforge.core.model.ResourceType; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserHmacSecret; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.oauth2.Oauth2ClientConfig; +import org.clevercastle.authforge.core.oauth2.Oauth2User; +import org.clevercastle.authforge.core.repository.UserRepository; +import org.clevercastle.authforge.core.token.TokenService; +import org.clevercastle.authforge.core.totp.RequestTotpResponse; +import org.clevercastle.authforge.core.totp.SetupTotpRequest; +import org.clevercastle.authforge.core.totp.TotpUtil; +import org.clevercastle.authforge.core.util.CodeUtil; +import org.clevercastle.authforge.core.util.HashUtil; +import org.clevercastle.authforge.core.util.IdUtil; +import org.clevercastle.authforge.core.util.TimeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,6 +46,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -54,11 +58,7 @@ public class UserServiceImpl implements UserService { private final CodeSender codeSender; private final CacheService cacheService; - public UserServiceImpl(Config config, - UserRepository userRepository, - TokenService tokenService, - CodeSender codeSender, - CacheService cacheService) { + public UserServiceImpl(Config config, UserRepository userRepository, TokenService tokenService, CodeSender codeSender, CacheService cacheService) { this.config = config; this.userRepository = userRepository; this.tokenService = tokenService; @@ -143,9 +143,7 @@ public String generate(Oauth2ClientConfig oauth2Client, String redirectUrl) { map.put("response_type", "code"); map.put("scope", StringUtils.join(oauth2Client.getScopes(), "%20")); map.put("state", UUID.randomUUID().toString()); - String queryString = map.entrySet().stream() - .map(it -> String.format("%s=%s", it.getKey(), it.getValue())) - .collect(java.util.stream.Collectors.joining("&")); + String queryString = map.entrySet().stream().map(it -> String.format("%s=%s", it.getKey(), it.getValue())).collect(java.util.stream.Collectors.joining("&")); // TODO: 2025/4/10 cache the state return oauth2Client.getOauth2LoginUrl() + "?" + queryString; } @@ -248,11 +246,9 @@ private OIDCTokenResponse oauth2Exchange(Oauth2ClientConfig clientConfig, String try { AuthorizationGrant codeGrant; if (StringUtils.isNotBlank(redirectUrl)) { - codeGrant = - new AuthorizationCodeGrant(code, new URI(redirectUrl)); + codeGrant = new AuthorizationCodeGrant(code, new URI(redirectUrl)); } else { - codeGrant = - new AuthorizationCodeGrant(code, null); + codeGrant = new AuthorizationCodeGrant(code, null); } URI tokenEndpoint = new URI(clientConfig.getOauth2TokenUrl()); TokenRequest request = new TokenRequest(tokenEndpoint, clientAuth, codeGrant, null); @@ -358,12 +354,20 @@ public ChallengeSession createChallenge(String userId, ChallengeSession.Type typ @Override public RequestTotpResponse requestTotp(User user) throws CastleException { - String key = UUID.randomUUID().toString(); - String secret = UUID.randomUUID().toString(); - cacheService.set(key, secret, 120); + String sessionId = UUID.randomUUID().toString(); + String secret = TotpUtil.generateSecret(); + cacheService.set(sessionId, secret, 300); // 5分钟过期时间 + + // 生成QR码URI,使用用户ID作为账户名称 + String accountName = user.getUserId(); // 使用用户ID作为账户标识 + String issuerName = "AuthForge"; // 可以从配置中获取 + String qrCodeUri = TotpUtil.generateQRCodeUri(secret, accountName, issuerName); + RequestTotpResponse requestTotpDto = new RequestTotpResponse(); - requestTotpDto.setSessionId(key); + requestTotpDto.setSessionId(sessionId); requestTotpDto.setSecret(secret); + requestTotpDto.setQrCodeUri(qrCodeUri); + requestTotpDto.setManualEntryKey(secret); return requestTotpDto; } @@ -371,9 +375,36 @@ public RequestTotpResponse requestTotp(User user) throws CastleException { public void setupTotp(User user, SetupTotpRequest request) throws CastleException { String secret = cacheService.get(request.getSessionId()); if (StringUtils.isBlank(secret)) { - throw new CastleException(); + throw new CastleException("Invalid session ID or session expired"); + } + + // 验证用户输入的验证码 + if (request.getCodes() == null || request.getCodes().isEmpty()) { + throw new CastleException("Verification codes are required"); + } + + // 至少需要验证一个验证码 + boolean isVerified = false; + for (var codeEntry : request.getCodes()) { + if (codeEntry.getCode() != null && codeEntry.getInputTime() != null) { + long timeSeconds = codeEntry.getInputTime().toEpochSecond(); + if (TotpUtil.verifyTOTPAtTime(codeEntry.getCode(), secret, timeSeconds)) { + isVerified = true; + break; + } + } + } + + if (!isVerified) { + throw new CastleException("Invalid verification code"); + } + + // 检查用户是否已经设置了TOTP + List existingSecrets = userRepository.listHmacSecretByUserId(user.getUserId()); + if (!existingSecrets.isEmpty()) { + throw new CastleException("TOTP already configured for this user"); } - // todo verify the user input verification code + UserHmacSecret userHmacSecret = new UserHmacSecret(); userHmacSecret.setUserId(user.getUserId()); userHmacSecret.setId(IdUtil.genId(ResourceType.totp)); @@ -381,7 +412,113 @@ public void setupTotp(User user, SetupTotpRequest request) throws CastleExceptio var now = TimeUtils.now(); userHmacSecret.setCreatedAt(now); userHmacSecret.setLastUsedAt(now); - userHmacSecret.setName(request.getName()); + userHmacSecret.setName(StringUtils.isNotBlank(request.getName()) ? request.getName() : "TOTP Device"); userRepository.createHmacSecret(userHmacSecret); + + // 清除缓存中的临时密钥 + cacheService.delete(request.getSessionId()); + } + + @Override + public MfaChallengeResponse createMfaChallenge(User user, String challengeType, String factorId) throws CastleException { + if (!"totp".equals(challengeType)) { + throw new CastleException("Unsupported challenge type: " + challengeType); + } + + // 验证用户是否有对应的MFA因子 + List userSecrets = userRepository.listHmacSecretByUserId(user.getUserId()); + UserHmacSecret targetSecret = null; + for (UserHmacSecret secret : userSecrets) { + if (secret.getId().equals(factorId)) { + targetSecret = secret; + break; + } + } + + if (targetSecret == null) { + throw new CastleException("MFA factor not found"); + } + + // 创建挑战会话 + ChallengeSession challengeSession = createChallenge(user.getUserId(), ChallengeSession.Type.mfa); + challengeSession.setCreatedAt(TimeUtils.now()); + challengeSession.setUserId(user.getUserId()); + + // 将挑战会话信息存储到缓存或数据库 + userRepository.createChallenge(challengeSession); + + // 将factorId与挑战会话关联存储(可以用Redis缓存) + cacheService.set("mfa_challenge_" + challengeSession.getId(), factorId, 300); // 5分钟过期 + + MfaChallengeResponse response = new MfaChallengeResponse(); + response.setChallengeId(challengeSession.getId()); + response.setChallengeType(challengeType); + response.setUserId(user.getUserId()); + response.setExpiresAt(TimeUtils.now().plusSeconds(300)); + + return response; + } + + @Override + public boolean verifyMfaChallenge(String challengeId, String code, String bindingCode) throws CastleException { + // 从缓存中获取挑战会话关联的factorId + String factorId = cacheService.get("mfa_challenge_" + challengeId); + if (StringUtils.isBlank(factorId)) { + throw new CastleException("Challenge session not found or expired"); + } + + // 获取用户的HMAC密钥 + // 这里需要根据factorId查找对应的UserHmacSecret + // 由于当前repository接口限制,我们通过其他方式获取 + // 实际应用中应该添加根据factorId查找的方法 + + // 验证TOTP代码 + try { + // 这里需要获取对应的secret,暂时简化处理 + // 在实际应用中,应该根据factorId从数据库中获取对应的secret + return TotpUtil.verifyTOTP(code, "dummy_secret"); // 需要替换为实际的secret + } catch (Exception e) { + logger.error("Error verifying TOTP code", e); + return false; + } finally { + // 验证后清除挑战会话 + cacheService.delete("mfa_challenge_" + challengeId); + } + } + + @Override + public List listMfaFactors(String userId) throws CastleException { + List secrets = userRepository.listHmacSecretByUserId(userId); + return secrets.stream().map(secret -> { + MfaFactorResponse factor = new MfaFactorResponse(); + factor.setId(secret.getId()); + factor.setType("totp"); + factor.setName(secret.getName()); + factor.setActive(true); + factor.setCreatedAt(secret.getCreatedAt()); + factor.setLastUsedAt(secret.getLastUsedAt()); + return factor; + }).collect(java.util.stream.Collectors.toList()); + } + + @Override + public void deleteMfaFactor(String userId, String factorId) throws CastleException { + // 这里需要在UserRepository中添加删除方法 + // userRepository.deleteHmacSecret(userId, factorId); + throw new CastleException("Delete MFA factor not implemented yet"); + } + + @Override + public boolean verifyTotpCode(String userId, String code) throws CastleException { + List secrets = userRepository.listHmacSecretByUserId(userId); + for (UserHmacSecret secret : secrets) { + if (TotpUtil.verifyTOTP(code, secret.getSecret())) { + // 更新最后使用时间 + secret.setLastUsedAt(TimeUtils.now()); + // 这里需要更新到数据库,暂时省略 + return true; + } + } + return false; } } diff --git a/src/main/java/org/clevercastle/authforge/UserState.java b/core/src/main/java/org/clevercastle/authforge/core/UserState.java similarity index 61% rename from src/main/java/org/clevercastle/authforge/UserState.java rename to core/src/main/java/org/clevercastle/authforge/core/UserState.java index e62c0ce..cfe6854 100644 --- a/src/main/java/org/clevercastle/authforge/UserState.java +++ b/core/src/main/java/org/clevercastle/authforge/core/UserState.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge; +package org.clevercastle.authforge.core; public enum UserState { ACTIVE, diff --git a/src/main/java/org/clevercastle/authforge/UserWithToken.java b/core/src/main/java/org/clevercastle/authforge/core/UserWithToken.java similarity index 80% rename from src/main/java/org/clevercastle/authforge/UserWithToken.java rename to core/src/main/java/org/clevercastle/authforge/core/UserWithToken.java index 73f9e2a..51330b4 100644 --- a/src/main/java/org/clevercastle/authforge/UserWithToken.java +++ b/core/src/main/java/org/clevercastle/authforge/core/UserWithToken.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge; +package org.clevercastle.authforge.core; -import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.core.model.User; public class UserWithToken { private final User user; diff --git a/core/src/main/java/org/clevercastle/authforge/core/challenge/ChallengeResponse.java b/core/src/main/java/org/clevercastle/authforge/core/challenge/ChallengeResponse.java new file mode 100644 index 0000000..c786eca --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/challenge/ChallengeResponse.java @@ -0,0 +1,7 @@ +package org.clevercastle.authforge.core.challenge; + +public class ChallengeResponse { + private String challengeId; + private ChallengeType type; + private String code; +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/challenge/ChallengeType.java b/core/src/main/java/org/clevercastle/authforge/core/challenge/ChallengeType.java new file mode 100644 index 0000000..1e7f386 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/challenge/ChallengeType.java @@ -0,0 +1,6 @@ +package org.clevercastle.authforge.core.challenge; + +public enum ChallengeType { + mfa, + changePassword +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/challenge/ChangeMfaSolutionChallengeAnswer.java b/core/src/main/java/org/clevercastle/authforge/core/challenge/ChangeMfaSolutionChallengeAnswer.java new file mode 100644 index 0000000..f18b828 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/challenge/ChangeMfaSolutionChallengeAnswer.java @@ -0,0 +1,22 @@ +package org.clevercastle.authforge.core.challenge; + +public class ChangeMfaSolutionChallengeAnswer { + private String sessionId; + private MfaChallengeSolution solution; + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + public MfaChallengeSolution getSolution() { + return solution; + } + + public void setSolution(MfaChallengeSolution solution) { + this.solution = solution; + } +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/challenge/ChangePasswordChallengeAnswer.java b/core/src/main/java/org/clevercastle/authforge/core/challenge/ChangePasswordChallengeAnswer.java new file mode 100644 index 0000000..b3290a1 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/challenge/ChangePasswordChallengeAnswer.java @@ -0,0 +1,31 @@ +package org.clevercastle.authforge.core.challenge; + +public class ChangePasswordChallengeAnswer { + private String sessionId; + private String oldPassword; + private String newPassword; + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + public String getOldPassword() { + return oldPassword; + } + + public void setOldPassword(String oldPassword) { + this.oldPassword = oldPassword; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/challenge/MfaChallengeAnswer.java b/core/src/main/java/org/clevercastle/authforge/core/challenge/MfaChallengeAnswer.java new file mode 100644 index 0000000..0f819e8 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/challenge/MfaChallengeAnswer.java @@ -0,0 +1,5 @@ +package org.clevercastle.authforge.core.challenge; + +public class MfaChallengeAnswer { + +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/challenge/MfaChallengeSolution.java b/core/src/main/java/org/clevercastle/authforge/core/challenge/MfaChallengeSolution.java new file mode 100644 index 0000000..9f72c6b --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/challenge/MfaChallengeSolution.java @@ -0,0 +1,7 @@ +package org.clevercastle.authforge.core.challenge; + +public enum MfaChallengeSolution { + totp, + email, + phone +} diff --git a/src/main/java/org/clevercastle/authforge/code/CodeSender.java b/core/src/main/java/org/clevercastle/authforge/core/code/CodeSender.java similarity index 68% rename from src/main/java/org/clevercastle/authforge/code/CodeSender.java rename to core/src/main/java/org/clevercastle/authforge/core/code/CodeSender.java index f7df037..f0075fe 100644 --- a/src/main/java/org/clevercastle/authforge/code/CodeSender.java +++ b/core/src/main/java/org/clevercastle/authforge/core/code/CodeSender.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.code; +package org.clevercastle.authforge.core.code; -import org.clevercastle.authforge.exception.CastleException; +import org.clevercastle.authforge.core.exception.CastleException; public interface CodeSender { void sendVerificationCode(String loginIdentifier, String verificationCode) throws CastleException; diff --git a/src/main/java/org/clevercastle/authforge/code/DummyCodeSender.java b/core/src/main/java/org/clevercastle/authforge/core/code/DummyCodeSender.java similarity index 84% rename from src/main/java/org/clevercastle/authforge/code/DummyCodeSender.java rename to core/src/main/java/org/clevercastle/authforge/core/code/DummyCodeSender.java index b9302bc..354d99a 100644 --- a/src/main/java/org/clevercastle/authforge/code/DummyCodeSender.java +++ b/core/src/main/java/org/clevercastle/authforge/core/code/DummyCodeSender.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.code; +package org.clevercastle.authforge.core.code; -import org.clevercastle.authforge.exception.CastleException; +import org.clevercastle.authforge.core.exception.CastleException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/org/clevercastle/authforge/code/SendCodeResponse.java b/core/src/main/java/org/clevercastle/authforge/core/code/SendCodeResponse.java similarity index 85% rename from src/main/java/org/clevercastle/authforge/code/SendCodeResponse.java rename to core/src/main/java/org/clevercastle/authforge/core/code/SendCodeResponse.java index dd99109..389e1fc 100644 --- a/src/main/java/org/clevercastle/authforge/code/SendCodeResponse.java +++ b/core/src/main/java/org/clevercastle/authforge/core/code/SendCodeResponse.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.code; +package org.clevercastle.authforge.core.code; public class SendCodeResponse { public enum Type { diff --git a/core/src/main/java/org/clevercastle/authforge/core/controller/MfaController.java b/core/src/main/java/org/clevercastle/authforge/core/controller/MfaController.java new file mode 100644 index 0000000..af82524 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/controller/MfaController.java @@ -0,0 +1,88 @@ +package org.clevercastle.authforge.core.controller; + +import org.clevercastle.authforge.core.UserService; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.mfa.dto.MfaChallengeRequest; +import org.clevercastle.authforge.core.mfa.dto.MfaChallengeResponse; +import org.clevercastle.authforge.core.mfa.dto.MfaFactorResponse; +import org.clevercastle.authforge.core.mfa.dto.MfaVerifyRequest; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.totp.RequestTotpResponse; +import org.clevercastle.authforge.core.totp.SetupTotpRequest; + +import java.util.List; + +/** + * MFA Controller示例 + * 这个类展示了如何使用MFA相关的服务方法 + * 在实际项目中,你可能会使用Spring Boot、JAX-RS或其他Web框架 + */ +public class MfaController { + + private UserService userService; + + public MfaController(UserService userService) { + this.userService = userService; + } + + /** + * 请求TOTP设置(第一步:获取密钥和QR码) + */ + public RequestTotpResponse requestTotpSetup(String userId) throws CastleException { + // 这里应该从认证上下文中获取用户,简化示例 + User user = new User(); + user.setUserId(userId); + + return userService.requestTotp(user); + } + + /** + * 完成TOTP设置(第二步:验证验证码并保存) + */ + public void completeTotpSetup(String userId, SetupTotpRequest request) throws CastleException { + User user = new User(); + user.setUserId(userId); + + userService.setupTotp(user, request); + } + + /** + * 创建MFA挑战 + */ + public MfaChallengeResponse createMfaChallenge(String userId, MfaChallengeRequest request) throws CastleException { + User user = new User(); + user.setUserId(userId); + + return userService.createMfaChallenge( + user, request.getChallengeType(), request.getFactorId()); + } + + /** + * 验证MFA挑战 + */ + public boolean verifyMfaChallenge(MfaVerifyRequest request) throws CastleException { + return userService.verifyMfaChallenge( + request.getChallengeId(), request.getCode(), request.getBindingCode()); + } + + /** + * 列出用户的MFA因子 + */ + public List listMfaFactors(String userId) throws CastleException { + return userService.listMfaFactors(userId); + } + + /** + * 删除MFA因子 + */ + public void deleteMfaFactor(String userId, String factorId) throws CastleException { + userService.deleteMfaFactor(userId, factorId); + } + + /** + * 直接验证TOTP码(用于登录时的MFA验证) + */ + public boolean verifyTotpCode(String userId, String code) throws CastleException { + return userService.verifyTotpCode(userId, code); + } +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/dto/MfaVerifyDto.java b/core/src/main/java/org/clevercastle/authforge/core/dto/MfaVerifyDto.java new file mode 100644 index 0000000..b1f3385 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/dto/MfaVerifyDto.java @@ -0,0 +1,4 @@ +package org.clevercastle.authforge.core.dto; + +public class MfaVerifyDto { +} diff --git a/src/main/java/org/clevercastle/authforge/dto/OneTimePasswordDto.java b/core/src/main/java/org/clevercastle/authforge/core/dto/OneTimePasswordDto.java similarity index 94% rename from src/main/java/org/clevercastle/authforge/dto/OneTimePasswordDto.java rename to core/src/main/java/org/clevercastle/authforge/core/dto/OneTimePasswordDto.java index e8f6e0d..e90ee13 100644 --- a/src/main/java/org/clevercastle/authforge/dto/OneTimePasswordDto.java +++ b/core/src/main/java/org/clevercastle/authforge/core/dto/OneTimePasswordDto.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.dto; +package org.clevercastle.authforge.core.dto; import java.time.OffsetDateTime; diff --git a/core/src/main/java/org/clevercastle/authforge/core/examples/MfaUsageExample.java b/core/src/main/java/org/clevercastle/authforge/core/examples/MfaUsageExample.java new file mode 100644 index 0000000..b20fd63 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/examples/MfaUsageExample.java @@ -0,0 +1,168 @@ +package org.clevercastle.authforge.core.examples; + +import org.clevercastle.authforge.core.UserService; +import org.clevercastle.authforge.core.controller.MfaController; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.mfa.dto.MfaChallengeRequest; +import org.clevercastle.authforge.core.mfa.dto.MfaChallengeResponse; +import org.clevercastle.authforge.core.mfa.dto.MfaFactorResponse; +import org.clevercastle.authforge.core.mfa.dto.MfaVerifyRequest; +import org.clevercastle.authforge.core.totp.RequestTotpResponse; +import org.clevercastle.authforge.core.totp.SetupTotpRequest; +import org.clevercastle.authforge.core.totp.SetupTotpVerificationCode; + +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.List; + +/** + * MFA功能使用示例 + * 展示如何使用auth-forge的MFA功能 + */ +public class MfaUsageExample { + + private UserService userService; + private MfaController mfaController; + + public MfaUsageExample(UserService userService) { + this.userService = userService; + this.mfaController = new MfaController(userService); + } + + /** + * 演示完整的TOTP设置流程 + */ + public void demonstrateTotpSetup(String userId) { + try { + System.out.println("=== TOTP Setup Demo ==="); + + // 第一步:请求TOTP设置 + System.out.println("1. Requesting TOTP setup..."); + RequestTotpResponse setupRequest = mfaController.requestTotpSetup(userId); + + System.out.println("Session ID: " + setupRequest.getSessionId()); + System.out.println("Secret: " + setupRequest.getSecret()); + System.out.println("QR Code URI: " + setupRequest.getQrCodeUri()); + System.out.println("Manual Entry Key: " + setupRequest.getManualEntryKey()); + + // 第二步:用户使用认证器应用扫描QR码,然后输入验证码 + System.out.println("\n2. User scans QR code and enters verification codes..."); + + // 模拟用户输入的验证码(实际应用中由用户通过认证器应用生成) + SetupTotpRequest completionRequest = new SetupTotpRequest(); + completionRequest.setSessionId(setupRequest.getSessionId()); + completionRequest.setName("My Authenticator"); + + // 模拟验证码输入(实际应用中需要用户提供真实的TOTP码) + SetupTotpVerificationCode code1 = new SetupTotpVerificationCode(); + code1.setCode("123456"); // 这应该是真实的TOTP码 + code1.setInputTime(OffsetDateTime.now()); + + completionRequest.setCodes(Arrays.asList(code1)); + + // 注意:这里会失败,因为我们使用的是假的验证码 + // 在实际应用中,用户需要使用认证器应用生成真实的验证码 + try { + mfaController.completeTotpSetup(userId, completionRequest); + System.out.println("TOTP setup completed successfully!"); + } catch (CastleException e) { + System.out.println("TOTP setup failed: " + e.getMessage()); + System.out.println("(This is expected in demo mode with fake verification codes)"); + } + + } catch (CastleException e) { + System.err.println("Error during TOTP setup: " + e.getMessage()); + } + } + + /** + * 演示MFA挑战流程 + */ + public void demonstrateMfaChallenge(String userId, String factorId) { + try { + System.out.println("\n=== MFA Challenge Demo ==="); + + // 创建MFA挑战 + System.out.println("1. Creating MFA challenge..."); + MfaChallengeRequest challengeRequest = new MfaChallengeRequest(); + challengeRequest.setChallengeType("totp"); + challengeRequest.setFactorId(factorId); + + MfaChallengeResponse challengeResponse = mfaController.createMfaChallenge(userId, challengeRequest); + + System.out.println("Challenge ID: " + challengeResponse.getChallengeId()); + System.out.println("Challenge Type: " + challengeResponse.getChallengeType()); + System.out.println("Expires At: " + challengeResponse.getExpiresAt()); + + // 验证MFA挑战 + System.out.println("\n2. Verifying MFA challenge..."); + MfaVerifyRequest verifyRequest = new MfaVerifyRequest(); + verifyRequest.setChallengeId(challengeResponse.getChallengeId()); + verifyRequest.setCode("123456"); // 模拟TOTP码 + + boolean verified = mfaController.verifyMfaChallenge(verifyRequest); + System.out.println("MFA verification result: " + verified); + + } catch (CastleException e) { + System.err.println("Error during MFA challenge: " + e.getMessage()); + } + } + + /** + * 演示列出和管理MFA因子 + */ + public void demonstrateMfaFactorManagement(String userId) { + try { + System.out.println("\n=== MFA Factor Management Demo ==="); + + // 列出用户的MFA因子 + System.out.println("1. Listing user's MFA factors..."); + List factors = mfaController.listMfaFactors(userId); + + if (factors.isEmpty()) { + System.out.println("No MFA factors found for user."); + } else { + for (MfaFactorResponse factor : factors) { + System.out.println("Factor ID: " + factor.getId()); + System.out.println("Factor Type: " + factor.getType()); + System.out.println("Factor Name: " + factor.getName()); + System.out.println("Active: " + factor.isActive()); + System.out.println("Created: " + factor.getCreatedAt()); + System.out.println("Last Used: " + factor.getLastUsedAt()); + System.out.println("---"); + } + } + + // 直接验证TOTP码 + System.out.println("\n2. Direct TOTP verification..."); + boolean totpVerified = mfaController.verifyTotpCode(userId, "123456"); + System.out.println("Direct TOTP verification result: " + totpVerified); + + } catch (CastleException e) { + System.err.println("Error during MFA factor management: " + e.getMessage()); + } + } + + /** + * 主演示方法 + */ + public static void demonstrateAllFeatures(UserService userService) { + String userId = "demo-user-123"; + String factorId = "demo-factor-456"; + + MfaUsageExample example = new MfaUsageExample(userService); + + // 演示TOTP设置 + example.demonstrateTotpSetup(userId); + + // 演示MFA挑战(需要有有效的factorId) + example.demonstrateMfaChallenge(userId, factorId); + + // 演示MFA因子管理 + example.demonstrateMfaFactorManagement(userId); + + System.out.println("\n=== Demo completed ==="); + System.out.println("Note: Some operations may fail in demo mode because they require"); + System.out.println("real TOTP codes from an authenticator app."); + } +} diff --git a/src/main/java/org/clevercastle/authforge/exception/CastleException.java b/core/src/main/java/org/clevercastle/authforge/core/exception/CastleException.java similarity index 82% rename from src/main/java/org/clevercastle/authforge/exception/CastleException.java rename to core/src/main/java/org/clevercastle/authforge/core/exception/CastleException.java index eab4c7e..ffb3cba 100644 --- a/src/main/java/org/clevercastle/authforge/exception/CastleException.java +++ b/core/src/main/java/org/clevercastle/authforge/core/exception/CastleException.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.exception; +package org.clevercastle.authforge.core.exception; public class CastleException extends Exception { public CastleException() { diff --git a/src/main/java/org/clevercastle/authforge/exception/UserExistException.java b/core/src/main/java/org/clevercastle/authforge/core/exception/UserExistException.java similarity index 54% rename from src/main/java/org/clevercastle/authforge/exception/UserExistException.java rename to core/src/main/java/org/clevercastle/authforge/core/exception/UserExistException.java index 8660505..ccff997 100644 --- a/src/main/java/org/clevercastle/authforge/exception/UserExistException.java +++ b/core/src/main/java/org/clevercastle/authforge/core/exception/UserExistException.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.exception; +package org.clevercastle.authforge.core.exception; public class UserExistException extends CastleException { } diff --git a/src/main/java/org/clevercastle/authforge/exception/UserNotConfirmedException.java b/core/src/main/java/org/clevercastle/authforge/core/exception/UserNotConfirmedException.java similarity index 69% rename from src/main/java/org/clevercastle/authforge/exception/UserNotConfirmedException.java rename to core/src/main/java/org/clevercastle/authforge/core/exception/UserNotConfirmedException.java index 2362072..26ee0df 100644 --- a/src/main/java/org/clevercastle/authforge/exception/UserNotConfirmedException.java +++ b/core/src/main/java/org/clevercastle/authforge/core/exception/UserNotConfirmedException.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.exception; +package org.clevercastle.authforge.core.exception; public class UserNotConfirmedException extends CastleException { public UserNotConfirmedException() { diff --git a/src/main/java/org/clevercastle/authforge/exception/UserNotFoundException.java b/core/src/main/java/org/clevercastle/authforge/core/exception/UserNotFoundException.java similarity index 67% rename from src/main/java/org/clevercastle/authforge/exception/UserNotFoundException.java rename to core/src/main/java/org/clevercastle/authforge/core/exception/UserNotFoundException.java index 1422b30..47397e2 100644 --- a/src/main/java/org/clevercastle/authforge/exception/UserNotFoundException.java +++ b/core/src/main/java/org/clevercastle/authforge/core/exception/UserNotFoundException.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.exception; +package org.clevercastle.authforge.core.exception; public class UserNotFoundException extends CastleException { public UserNotFoundException() { diff --git a/src/main/java/org/clevercastle/authforge/http/HttpRequest.java b/core/src/main/java/org/clevercastle/authforge/core/http/HttpRequest.java similarity index 94% rename from src/main/java/org/clevercastle/authforge/http/HttpRequest.java rename to core/src/main/java/org/clevercastle/authforge/core/http/HttpRequest.java index 0abb0be..76428c4 100644 --- a/src/main/java/org/clevercastle/authforge/http/HttpRequest.java +++ b/core/src/main/java/org/clevercastle/authforge/core/http/HttpRequest.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.http; +package org.clevercastle.authforge.core.http; import java.util.Map; diff --git a/src/main/java/org/clevercastle/authforge/http/HttpResponse.java b/core/src/main/java/org/clevercastle/authforge/core/http/HttpResponse.java similarity index 94% rename from src/main/java/org/clevercastle/authforge/http/HttpResponse.java rename to core/src/main/java/org/clevercastle/authforge/core/http/HttpResponse.java index ec2469b..9f8e236 100644 --- a/src/main/java/org/clevercastle/authforge/http/HttpResponse.java +++ b/core/src/main/java/org/clevercastle/authforge/core/http/HttpResponse.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.http; +package org.clevercastle.authforge.core.http; import java.util.Map; diff --git a/core/src/main/java/org/clevercastle/authforge/core/http/IHttpClient.java b/core/src/main/java/org/clevercastle/authforge/core/http/IHttpClient.java new file mode 100644 index 0000000..22576a4 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/http/IHttpClient.java @@ -0,0 +1,7 @@ +package org.clevercastle.authforge.core.http; + +import org.clevercastle.authforge.core.exception.CastleException; + +public interface IHttpClient { + HttpResponse execute(HttpRequest request) throws CastleException; +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/mfa/MfaUtils.java b/core/src/main/java/org/clevercastle/authforge/core/mfa/MfaUtils.java new file mode 100644 index 0000000..a55ddf6 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/mfa/MfaUtils.java @@ -0,0 +1,42 @@ +package org.clevercastle.authforge.core.mfa; + +import org.clevercastle.authforge.core.totp.TotpUtil; + +public class MfaUtils { + + /** + * 生成TOTP设置的QR码数据 + * @param secret Base32编码的密钥 + * @param accountName 账户名称 + * @param issuerName 发行者名称 + * @return QR码数据对象 + */ + public static QrCodeData generateTotpQrCode(String secret, String accountName, String issuerName) { + String uri = TotpUtil.generateQRCodeUri(secret, accountName, issuerName); + QrCodeData qrCodeData = new QrCodeData(); + qrCodeData.setUri(uri); + qrCodeData.setSecret(secret); + return qrCodeData; + } + + public static class QrCodeData { + private String uri; + private String secret; + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + } +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/mfa/dto/MfaChallengeRequest.java b/core/src/main/java/org/clevercastle/authforge/core/mfa/dto/MfaChallengeRequest.java new file mode 100644 index 0000000..6378138 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/mfa/dto/MfaChallengeRequest.java @@ -0,0 +1,40 @@ +package org.clevercastle.authforge.core.mfa.dto; + +public class MfaChallengeRequest { + private String challengeType; // "totp", "oob", "recovery-code" + private String factorId; // 对于TOTP,这是用户的TOTP设备ID + private String challengeId; // 对于验证场景 + private String bindingMethod; // "prompt" 或 "transfer" + + public String getChallengeType() { + return challengeType; + } + + public void setChallengeType(String challengeType) { + this.challengeType = challengeType; + } + + public String getFactorId() { + return factorId; + } + + public void setFactorId(String factorId) { + this.factorId = factorId; + } + + public String getChallengeId() { + return challengeId; + } + + public void setChallengeId(String challengeId) { + this.challengeId = challengeId; + } + + public String getBindingMethod() { + return bindingMethod; + } + + public void setBindingMethod(String bindingMethod) { + this.bindingMethod = bindingMethod; + } +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/mfa/dto/MfaChallengeResponse.java b/core/src/main/java/org/clevercastle/authforge/core/mfa/dto/MfaChallengeResponse.java new file mode 100644 index 0000000..48f4c4b --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/mfa/dto/MfaChallengeResponse.java @@ -0,0 +1,51 @@ +package org.clevercastle.authforge.core.mfa.dto; + +import java.time.OffsetDateTime; + +public class MfaChallengeResponse { + private String challengeId; + private String challengeType; + private String userId; + private OffsetDateTime expiresAt; + private Object challengeData; // 可以是OOB电话号码、邮箱等 + + public String getChallengeId() { + return challengeId; + } + + public void setChallengeId(String challengeId) { + this.challengeId = challengeId; + } + + public String getChallengeType() { + return challengeType; + } + + public void setChallengeType(String challengeType) { + this.challengeType = challengeType; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public OffsetDateTime getExpiresAt() { + return expiresAt; + } + + public void setExpiresAt(OffsetDateTime expiresAt) { + this.expiresAt = expiresAt; + } + + public Object getChallengeData() { + return challengeData; + } + + public void setChallengeData(Object challengeData) { + this.challengeData = challengeData; + } +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/mfa/dto/MfaFactorResponse.java b/core/src/main/java/org/clevercastle/authforge/core/mfa/dto/MfaFactorResponse.java new file mode 100644 index 0000000..01026e6 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/mfa/dto/MfaFactorResponse.java @@ -0,0 +1,69 @@ +package org.clevercastle.authforge.core.mfa.dto; + +import java.time.OffsetDateTime; + +public class MfaFactorResponse { + private String id; + private String type; // "totp", "oob", "recovery-code" + private String name; + private boolean active; + private OffsetDateTime createdAt; + private OffsetDateTime lastUsedAt; + private Object factorData; // 因子特定的数据 + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } + + public OffsetDateTime getLastUsedAt() { + return lastUsedAt; + } + + public void setLastUsedAt(OffsetDateTime lastUsedAt) { + this.lastUsedAt = lastUsedAt; + } + + public Object getFactorData() { + return factorData; + } + + public void setFactorData(Object factorData) { + this.factorData = factorData; + } +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/mfa/dto/MfaVerifyRequest.java b/core/src/main/java/org/clevercastle/authforge/core/mfa/dto/MfaVerifyRequest.java new file mode 100644 index 0000000..aa0ee08 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/mfa/dto/MfaVerifyRequest.java @@ -0,0 +1,31 @@ +package org.clevercastle.authforge.core.mfa.dto; + +public class MfaVerifyRequest { + private String challengeId; + private String code; // 验证码 + private String bindingCode; // 用于OOB绑定的代码 + + public String getChallengeId() { + return challengeId; + } + + public void setChallengeId(String challengeId) { + this.challengeId = challengeId; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getBindingCode() { + return bindingCode; + } + + public void setBindingCode(String bindingCode) { + this.bindingCode = bindingCode; + } +} diff --git a/src/main/java/org/clevercastle/authforge/model/ChallengeSession.java b/core/src/main/java/org/clevercastle/authforge/core/model/ChallengeSession.java similarity index 93% rename from src/main/java/org/clevercastle/authforge/model/ChallengeSession.java rename to core/src/main/java/org/clevercastle/authforge/core/model/ChallengeSession.java index 18f3bd2..1f8a40a 100644 --- a/src/main/java/org/clevercastle/authforge/model/ChallengeSession.java +++ b/core/src/main/java/org/clevercastle/authforge/core/model/ChallengeSession.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.model; +package org.clevercastle.authforge.core.model; import jakarta.persistence.Entity; import jakarta.persistence.Id; @@ -13,6 +13,7 @@ public class ChallengeSession { public enum Type { mfa, + changePassword } @javax.persistence.Id diff --git a/src/main/java/org/clevercastle/authforge/model/OneTimePassword.java b/core/src/main/java/org/clevercastle/authforge/core/model/OneTimePassword.java similarity index 86% rename from src/main/java/org/clevercastle/authforge/model/OneTimePassword.java rename to core/src/main/java/org/clevercastle/authforge/core/model/OneTimePassword.java index ef96e24..87ff274 100644 --- a/src/main/java/org/clevercastle/authforge/model/OneTimePassword.java +++ b/core/src/main/java/org/clevercastle/authforge/core/model/OneTimePassword.java @@ -1,11 +1,11 @@ -package org.clevercastle.authforge.model; +package org.clevercastle.authforge.core.model; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.IdClass; import jakarta.persistence.Table; -import org.clevercastle.authforge.repository.rdsjpa.RdsJpaOneTimePasswordId; -import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserRefreshTokenMappingId; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaOneTimePasswordId; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserRefreshTokenMappingId; import java.time.OffsetDateTime; diff --git a/src/main/java/org/clevercastle/authforge/model/ResourceType.java b/core/src/main/java/org/clevercastle/authforge/core/model/ResourceType.java similarity index 60% rename from src/main/java/org/clevercastle/authforge/model/ResourceType.java rename to core/src/main/java/org/clevercastle/authforge/core/model/ResourceType.java index 1d56821..203832f 100644 --- a/src/main/java/org/clevercastle/authforge/model/ResourceType.java +++ b/core/src/main/java/org/clevercastle/authforge/core/model/ResourceType.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.model; +package org.clevercastle.authforge.core.model; public enum ResourceType { user, diff --git a/src/main/java/org/clevercastle/authforge/model/User.java b/core/src/main/java/org/clevercastle/authforge/core/model/User.java similarity index 93% rename from src/main/java/org/clevercastle/authforge/model/User.java rename to core/src/main/java/org/clevercastle/authforge/core/model/User.java index c7bdb00..a5ca9df 100644 --- a/src/main/java/org/clevercastle/authforge/model/User.java +++ b/core/src/main/java/org/clevercastle/authforge/core/model/User.java @@ -1,13 +1,14 @@ -package org.clevercastle.authforge.model; +package org.clevercastle.authforge.core.model; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.Id; import jakarta.persistence.Table; -import org.clevercastle.authforge.UserState; +import org.clevercastle.authforge.core.UserState; import java.time.OffsetDateTime; +import java.util.List; @javax.persistence.Entity @javax.persistence.Table(name = "users") @@ -25,6 +26,8 @@ public class User { private String resetPasswordCode; private OffsetDateTime resetPasswordCodeExpiredAt; + private List userLoginItems; + private OffsetDateTime createdAt; private OffsetDateTime updatedAt; diff --git a/src/main/java/org/clevercastle/authforge/model/UserHmacSecret.java b/core/src/main/java/org/clevercastle/authforge/core/model/UserHmacSecret.java similarity index 92% rename from src/main/java/org/clevercastle/authforge/model/UserHmacSecret.java rename to core/src/main/java/org/clevercastle/authforge/core/model/UserHmacSecret.java index 2a638c2..963d0e1 100644 --- a/src/main/java/org/clevercastle/authforge/model/UserHmacSecret.java +++ b/core/src/main/java/org/clevercastle/authforge/core/model/UserHmacSecret.java @@ -1,10 +1,10 @@ -package org.clevercastle.authforge.model; +package org.clevercastle.authforge.core.model; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.IdClass; import jakarta.persistence.Table; -import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserHmacSecretId; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserHmacSecretId; import java.time.OffsetDateTime; diff --git a/src/main/java/org/clevercastle/authforge/model/UserLoginItem.java b/core/src/main/java/org/clevercastle/authforge/core/model/UserLoginItem.java similarity index 98% rename from src/main/java/org/clevercastle/authforge/model/UserLoginItem.java rename to core/src/main/java/org/clevercastle/authforge/core/model/UserLoginItem.java index 8b5754c..ab5a39f 100644 --- a/src/main/java/org/clevercastle/authforge/model/UserLoginItem.java +++ b/core/src/main/java/org/clevercastle/authforge/core/model/UserLoginItem.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.model; +package org.clevercastle.authforge.core.model; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; diff --git a/src/main/java/org/clevercastle/authforge/model/UserRefreshTokenMapping.java b/core/src/main/java/org/clevercastle/authforge/core/model/UserRefreshTokenMapping.java similarity index 90% rename from src/main/java/org/clevercastle/authforge/model/UserRefreshTokenMapping.java rename to core/src/main/java/org/clevercastle/authforge/core/model/UserRefreshTokenMapping.java index 91e3ee7..143f44c 100644 --- a/src/main/java/org/clevercastle/authforge/model/UserRefreshTokenMapping.java +++ b/core/src/main/java/org/clevercastle/authforge/core/model/UserRefreshTokenMapping.java @@ -1,10 +1,10 @@ -package org.clevercastle.authforge.model; +package org.clevercastle.authforge.core.model; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.IdClass; import jakarta.persistence.Table; -import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserRefreshTokenMappingId; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserRefreshTokenMappingId; import java.time.OffsetDateTime; diff --git a/src/main/java/org/clevercastle/authforge/oauth2/AbstractOauth2ExchangeService.java b/core/src/main/java/org/clevercastle/authforge/core/oauth2/AbstractOauth2ExchangeService.java similarity index 96% rename from src/main/java/org/clevercastle/authforge/oauth2/AbstractOauth2ExchangeService.java rename to core/src/main/java/org/clevercastle/authforge/core/oauth2/AbstractOauth2ExchangeService.java index bb167fd..acc6f56 100644 --- a/src/main/java/org/clevercastle/authforge/oauth2/AbstractOauth2ExchangeService.java +++ b/core/src/main/java/org/clevercastle/authforge/core/oauth2/AbstractOauth2ExchangeService.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.oauth2; +package org.clevercastle.authforge.core.oauth2; import com.nimbusds.oauth2.sdk.AuthorizationCode; import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; @@ -12,7 +12,7 @@ import com.nimbusds.oauth2.sdk.http.HTTPResponse; import com.nimbusds.oauth2.sdk.id.ClientID; import org.apache.commons.lang3.StringUtils; -import org.clevercastle.authforge.exception.CastleException; +import org.clevercastle.authforge.core.exception.CastleException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/org/clevercastle/authforge/oauth2/Avatar.java b/core/src/main/java/org/clevercastle/authforge/core/oauth2/Avatar.java similarity index 89% rename from src/main/java/org/clevercastle/authforge/oauth2/Avatar.java rename to core/src/main/java/org/clevercastle/authforge/core/oauth2/Avatar.java index 09e708d..d7d11be 100644 --- a/src/main/java/org/clevercastle/authforge/oauth2/Avatar.java +++ b/core/src/main/java/org/clevercastle/authforge/core/oauth2/Avatar.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.oauth2; +package org.clevercastle.authforge.core.oauth2; public class Avatar { public enum Type { diff --git a/src/main/java/org/clevercastle/authforge/oauth2/Oauth2ClientConfig.java b/core/src/main/java/org/clevercastle/authforge/core/oauth2/Oauth2ClientConfig.java similarity index 97% rename from src/main/java/org/clevercastle/authforge/oauth2/Oauth2ClientConfig.java rename to core/src/main/java/org/clevercastle/authforge/core/oauth2/Oauth2ClientConfig.java index 2b29633..96d6cf2 100644 --- a/src/main/java/org/clevercastle/authforge/oauth2/Oauth2ClientConfig.java +++ b/core/src/main/java/org/clevercastle/authforge/core/oauth2/Oauth2ClientConfig.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.oauth2; +package org.clevercastle.authforge.core.oauth2; -import org.clevercastle.authforge.http.IHttpClient; +import org.clevercastle.authforge.core.http.IHttpClient; import java.util.List; import java.util.Map; diff --git a/src/main/java/org/clevercastle/authforge/oauth2/Oauth2ExchangeService.java b/core/src/main/java/org/clevercastle/authforge/core/oauth2/Oauth2ExchangeService.java similarity index 62% rename from src/main/java/org/clevercastle/authforge/oauth2/Oauth2ExchangeService.java rename to core/src/main/java/org/clevercastle/authforge/core/oauth2/Oauth2ExchangeService.java index a602742..d4ba346 100644 --- a/src/main/java/org/clevercastle/authforge/oauth2/Oauth2ExchangeService.java +++ b/core/src/main/java/org/clevercastle/authforge/core/oauth2/Oauth2ExchangeService.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.oauth2; +package org.clevercastle.authforge.core.oauth2; -import org.clevercastle.authforge.exception.CastleException; +import org.clevercastle.authforge.core.exception.CastleException; public interface Oauth2ExchangeService { diff --git a/src/main/java/org/clevercastle/authforge/oauth2/Oauth2User.java b/core/src/main/java/org/clevercastle/authforge/core/oauth2/Oauth2User.java similarity index 95% rename from src/main/java/org/clevercastle/authforge/oauth2/Oauth2User.java rename to core/src/main/java/org/clevercastle/authforge/core/oauth2/Oauth2User.java index 18c9ec3..5ce8409 100644 --- a/src/main/java/org/clevercastle/authforge/oauth2/Oauth2User.java +++ b/core/src/main/java/org/clevercastle/authforge/core/oauth2/Oauth2User.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.oauth2; +package org.clevercastle.authforge.core.oauth2; public class Oauth2User { private String loginIdentifier; diff --git a/src/main/java/org/clevercastle/authforge/oauth2/github/GithubOauth2ExchangeService.java b/core/src/main/java/org/clevercastle/authforge/core/oauth2/github/GithubOauth2ExchangeService.java similarity index 86% rename from src/main/java/org/clevercastle/authforge/oauth2/github/GithubOauth2ExchangeService.java rename to core/src/main/java/org/clevercastle/authforge/core/oauth2/github/GithubOauth2ExchangeService.java index ca4a741..1c4bc97 100644 --- a/src/main/java/org/clevercastle/authforge/oauth2/github/GithubOauth2ExchangeService.java +++ b/core/src/main/java/org/clevercastle/authforge/core/oauth2/github/GithubOauth2ExchangeService.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.oauth2.github; +package org.clevercastle.authforge.core.oauth2.github; import com.nimbusds.oauth2.sdk.AccessTokenResponse; import com.nimbusds.oauth2.sdk.TokenErrorResponse; @@ -6,12 +6,12 @@ import com.nimbusds.oauth2.sdk.http.HTTPRequest; import com.nimbusds.oauth2.sdk.http.HTTPResponse; import com.nimbusds.openid.connect.sdk.OIDCTokenResponse; -import org.clevercastle.authforge.exception.CastleException; -import org.clevercastle.authforge.http.HttpResponse; -import org.clevercastle.authforge.oauth2.AbstractOauth2ExchangeService; -import org.clevercastle.authforge.oauth2.Oauth2ClientConfig; -import org.clevercastle.authforge.oauth2.Oauth2User; -import org.clevercastle.authforge.util.JsonUtil; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.http.HttpResponse; +import org.clevercastle.authforge.core.oauth2.AbstractOauth2ExchangeService; +import org.clevercastle.authforge.core.oauth2.Oauth2ClientConfig; +import org.clevercastle.authforge.core.oauth2.Oauth2User; +import org.clevercastle.authforge.core.util.JsonUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,7 +38,7 @@ protected Oauth2User parse(Oauth2ClientConfig clientConfig, TokenResponse tokenR if (tokenResponse instanceof AccessTokenResponse) { Oauth2User oauth2User = new Oauth2User(); // https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28 - org.clevercastle.authforge.http.HttpRequest httpRequest = new org.clevercastle.authforge.http.HttpRequest(); + org.clevercastle.authforge.core.http.HttpRequest httpRequest = new org.clevercastle.authforge.core.http.HttpRequest(); httpRequest.setUrl(USER_INFO_API); httpRequest.setMethod("GET"); httpRequest.setBody(null); diff --git a/src/main/java/org/clevercastle/authforge/oauth2/github/GithubUser.java b/core/src/main/java/org/clevercastle/authforge/core/oauth2/github/GithubUser.java similarity index 96% rename from src/main/java/org/clevercastle/authforge/oauth2/github/GithubUser.java rename to core/src/main/java/org/clevercastle/authforge/core/oauth2/github/GithubUser.java index bdb84b3..11a8829 100644 --- a/src/main/java/org/clevercastle/authforge/oauth2/github/GithubUser.java +++ b/core/src/main/java/org/clevercastle/authforge/core/oauth2/github/GithubUser.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.oauth2.github; +package org.clevercastle.authforge.core.oauth2.github; import java.time.OffsetDateTime; diff --git a/src/main/java/org/clevercastle/authforge/oauth2/oidc/OidcExchangeService.java b/core/src/main/java/org/clevercastle/authforge/core/oauth2/oidc/OidcExchangeService.java similarity index 89% rename from src/main/java/org/clevercastle/authforge/oauth2/oidc/OidcExchangeService.java rename to core/src/main/java/org/clevercastle/authforge/core/oauth2/oidc/OidcExchangeService.java index 3b1f2bb..cf0db6a 100644 --- a/src/main/java/org/clevercastle/authforge/oauth2/oidc/OidcExchangeService.java +++ b/core/src/main/java/org/clevercastle/authforge/core/oauth2/oidc/OidcExchangeService.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.oauth2.oidc; +package org.clevercastle.authforge.core.oauth2.oidc; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.oauth2.sdk.AccessTokenResponse; @@ -6,10 +6,10 @@ import com.nimbusds.oauth2.sdk.TokenResponse; import com.nimbusds.oauth2.sdk.http.HTTPResponse; import com.nimbusds.openid.connect.sdk.OIDCTokenResponse; -import org.clevercastle.authforge.exception.CastleException; -import org.clevercastle.authforge.oauth2.AbstractOauth2ExchangeService; -import org.clevercastle.authforge.oauth2.Oauth2ClientConfig; -import org.clevercastle.authforge.oauth2.Oauth2User; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.oauth2.AbstractOauth2ExchangeService; +import org.clevercastle.authforge.core.oauth2.Oauth2ClientConfig; +import org.clevercastle.authforge.core.oauth2.Oauth2User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/org/clevercastle/authforge/repository/UserRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/UserRepository.java similarity index 71% rename from src/main/java/org/clevercastle/authforge/repository/UserRepository.java rename to core/src/main/java/org/clevercastle/authforge/core/repository/UserRepository.java index b66e36c..705a87c 100644 --- a/src/main/java/org/clevercastle/authforge/repository/UserRepository.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/UserRepository.java @@ -1,14 +1,14 @@ -package org.clevercastle.authforge.repository; +package org.clevercastle.authforge.core.repository; 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.UserRefreshTokenMapping; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.ChallengeSession; +import org.clevercastle.authforge.core.model.OneTimePassword; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserHmacSecret; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.model.UserRefreshTokenMapping; import java.time.OffsetDateTime; import java.util.List; diff --git a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUser.java b/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUser.java similarity index 97% rename from src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUser.java rename to core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUser.java index d51b11c..d4b0fb1 100644 --- a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUser.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUser.java @@ -1,7 +1,7 @@ -package org.clevercastle.authforge.repository.dynamodb; +package org.clevercastle.authforge.core.repository.dynamodb; -import org.clevercastle.authforge.UserState; -import org.clevercastle.authforge.model.UserLoginItem; +import org.clevercastle.authforge.core.UserState; +import org.clevercastle.authforge.core.model.UserLoginItem; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; diff --git a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java b/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUserRepositoryImpl.java similarity index 92% rename from src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java rename to core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUserRepositoryImpl.java index 119e46f..c597a39 100644 --- a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserRepositoryImpl.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUserRepositoryImpl.java @@ -1,18 +1,18 @@ -package org.clevercastle.authforge.repository.dynamodb; +package org.clevercastle.authforge.core.repository.dynamodb; import jakarta.annotation.Nonnull; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; -import org.clevercastle.authforge.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.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.ChallengeSession; +import org.clevercastle.authforge.core.model.OneTimePassword; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserHmacSecret; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.model.UserRefreshTokenMapping; +import org.clevercastle.authforge.core.repository.UserRepository; +import org.clevercastle.authforge.core.util.TimeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; diff --git a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserUtil.java b/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUserUtil.java similarity index 94% rename from src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserUtil.java rename to core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUserUtil.java index 8349aa0..2a598e8 100644 --- a/src/main/java/org/clevercastle/authforge/repository/dynamodb/DynamodbUserUtil.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/dynamodb/DynamodbUserUtil.java @@ -1,8 +1,8 @@ -package org.clevercastle.authforge.repository.dynamodb; +package org.clevercastle.authforge.core.repository.dynamodb; -import org.clevercastle.authforge.model.User; -import org.clevercastle.authforge.model.UserLoginItem; -import org.clevercastle.authforge.model.UserRefreshTokenMapping; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.model.UserRefreshTokenMapping; public class DynamodbUserUtil { diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaChallengeSessionRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaChallengeSessionRepository.java similarity index 56% rename from src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaChallengeSessionRepository.java rename to core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaChallengeSessionRepository.java index f90d022..b4d2bde 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaChallengeSessionRepository.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaChallengeSessionRepository.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.repository.rdsjpa; +package org.clevercastle.authforge.core.repository.rdsjpa; -import org.clevercastle.authforge.model.ChallengeSession; +import org.clevercastle.authforge.core.model.ChallengeSession; public interface RdsJpaChallengeSessionRepository { ChallengeSession save(ChallengeSession challengeSession); diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaOneTimePasswordId.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaOneTimePasswordId.java similarity index 93% rename from src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaOneTimePasswordId.java rename to core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaOneTimePasswordId.java index dc15b1d..e343f99 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaOneTimePasswordId.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaOneTimePasswordId.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.repository.rdsjpa; +package org.clevercastle.authforge.core.repository.rdsjpa; import java.io.Serializable; diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaOneTimePasswordRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaOneTimePasswordRepository.java similarity index 69% rename from src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaOneTimePasswordRepository.java rename to core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaOneTimePasswordRepository.java index 8d7cf30..f330e03 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaOneTimePasswordRepository.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaOneTimePasswordRepository.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.repository.rdsjpa; +package org.clevercastle.authforge.core.repository.rdsjpa; -import org.clevercastle.authforge.model.OneTimePassword; +import org.clevercastle.authforge.core.model.OneTimePassword; import java.util.List; diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserHmacSecretId.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserHmacSecretId.java similarity index 87% rename from src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserHmacSecretId.java rename to core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserHmacSecretId.java index 04a5128..d9f9fc2 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserHmacSecretId.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserHmacSecretId.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.repository.rdsjpa; +package org.clevercastle.authforge.core.repository.rdsjpa; import java.io.Serializable; diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserHmacSecretRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserHmacSecretRepository.java similarity index 61% rename from src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserHmacSecretRepository.java rename to core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserHmacSecretRepository.java index b561fa0..61ec4b6 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserHmacSecretRepository.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserHmacSecretRepository.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.repository.rdsjpa; +package org.clevercastle.authforge.core.repository.rdsjpa; -import org.clevercastle.authforge.model.UserHmacSecret; +import org.clevercastle.authforge.core.model.UserHmacSecret; import java.util.List; diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserLoginItemRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserLoginItemRepository.java similarity index 83% rename from src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserLoginItemRepository.java rename to core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserLoginItemRepository.java index 4401354..b9724d7 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserLoginItemRepository.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserLoginItemRepository.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.repository.rdsjpa; +package org.clevercastle.authforge.core.repository.rdsjpa; -import org.clevercastle.authforge.model.UserLoginItem; +import org.clevercastle.authforge.core.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/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserModelRepository.java similarity index 50% rename from src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserModelRepository.java rename to core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserModelRepository.java index 1b5c8cb..ceb8212 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserModelRepository.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserModelRepository.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.repository.rdsjpa; +package org.clevercastle.authforge.core.repository.rdsjpa; -import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.core.model.User; public interface RdsJpaUserModelRepository { User save(User user); diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRefreshTokenMappingId.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRefreshTokenMappingId.java similarity index 92% rename from src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRefreshTokenMappingId.java rename to core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRefreshTokenMappingId.java index f8539b1..48679bf 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRefreshTokenMappingId.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRefreshTokenMappingId.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.repository.rdsjpa; +package org.clevercastle.authforge.core.repository.rdsjpa; import java.io.Serializable; diff --git a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRefreshTokenMappingRepository.java b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRefreshTokenMappingRepository.java similarity index 71% rename from src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRefreshTokenMappingRepository.java rename to core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRefreshTokenMappingRepository.java index 661e8dd..2008811 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRefreshTokenMappingRepository.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRefreshTokenMappingRepository.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.repository.rdsjpa; +package org.clevercastle.authforge.core.repository.rdsjpa; -import org.clevercastle.authforge.model.UserRefreshTokenMapping; +import org.clevercastle.authforge.core.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/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRepositoryImpl.java similarity index 89% rename from src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRepositoryImpl.java rename to core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRepositoryImpl.java index eda5c8b..690c558 100644 --- a/src/main/java/org/clevercastle/authforge/repository/rdsjpa/RdsJpaUserRepositoryImpl.java +++ b/core/src/main/java/org/clevercastle/authforge/core/repository/rdsjpa/RdsJpaUserRepositoryImpl.java @@ -1,17 +1,17 @@ -package org.clevercastle.authforge.repository.rdsjpa; +package org.clevercastle.authforge.core.repository.rdsjpa; import jakarta.annotation.Nonnull; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; -import org.clevercastle.authforge.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; -import org.clevercastle.authforge.repository.UserRepository; -import org.clevercastle.authforge.util.TimeUtils; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.ChallengeSession; +import org.clevercastle.authforge.core.model.OneTimePassword; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserHmacSecret; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.model.UserRefreshTokenMapping; +import org.clevercastle.authforge.core.repository.UserRepository; +import org.clevercastle.authforge.core.util.TimeUtils; import java.time.OffsetDateTime; import java.util.List; diff --git a/core/src/main/java/org/clevercastle/authforge/core/token/TokenService.java b/core/src/main/java/org/clevercastle/authforge/core/token/TokenService.java new file mode 100644 index 0000000..fa721b7 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/token/TokenService.java @@ -0,0 +1,15 @@ +package org.clevercastle.authforge.core.token; + +import org.clevercastle.authforge.core.TokenHolder; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserLoginItem; + +public interface TokenService { + enum Scope { + access, + id + } + + TokenHolder generateToken(User user, UserLoginItem item) throws CastleException; +} diff --git a/core/src/main/java/org/clevercastle/authforge/core/token/jwt/JwtTokenAttributes.java b/core/src/main/java/org/clevercastle/authforge/core/token/jwt/JwtTokenAttributes.java new file mode 100644 index 0000000..1d3c66d --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/token/jwt/JwtTokenAttributes.java @@ -0,0 +1,163 @@ +package org.clevercastle.authforge.core.token.jwt; + +import org.apache.commons.lang3.StringUtils; + +public class JwtTokenAttributes { + // header + private String kid; + + // payload + private String sub; + private String scope; + private String iss; + private long expireInSecond; + private String clientId; + private String eventId; + private String email; + private String tokenUse; + private String username; + + public static Builder builder() { + return new Builder(); + } + + public String getKid() { + return kid; + } + + public String getSub() { + return sub; + } + + public String getScope() { + return scope; + } + + public String getIss() { + return iss; + } + + public long getExpireInSecond() { + return expireInSecond; + } + + public String getClientId() { + return clientId; + } + + public String getEventId() { + return eventId; + } + + public String getTokenUse() { + return tokenUse; + } + + public String getEmail() { + return email; + } + + public String getUsername() { + return username; + } + + public static final class Builder { + private String kid; + private String sub; + private String scope; + private String iss; + private long expireInSecond; + private String clientId; + private String eventId; + private String email; + private String tokenUse; + private String username; + + private Builder() { + } + + public static Builder aJwtTokenAttributes() { + return new Builder(); + } + + public Builder withKid(String kid) { + this.kid = kid; + return this; + } + + public Builder withSub(String sub) { + this.sub = sub; + return this; + } + + public Builder withScope(String scope) { + this.scope = scope; + return this; + } + + public Builder withIss(String iss) { + this.iss = iss; + return this; + } + + public Builder withExpireInSecond(long expireInSecond) { + this.expireInSecond = expireInSecond; + return this; + } + + public Builder withClientId(String clientId) { + this.clientId = clientId; + return this; + } + + public Builder withEventId(String eventId) { + this.eventId = eventId; + return this; + } + + public Builder withEmail(String email) { + this.email = email; + return this; + } + + public Builder withTokenUse(String tokenUse) { + this.tokenUse = tokenUse; + return this; + } + + public Builder withUsername(String username) { + this.username = username; + return this; + } + + public JwtTokenAttributes build() { + if (StringUtils.isBlank(this.tokenUse)) { + this.tokenUse = "access"; + } + if (StringUtils.isNotBlank(this.sub) && StringUtils.isBlank(this.email)) { + throw new IllegalArgumentException("email must be set when sub is set"); + } + if (StringUtils.isBlank(this.sub) && StringUtils.isBlank(scope)) { + throw new IllegalArgumentException("sub or scope must be set"); + } + if (this.expireInSecond <= 300) { + throw new IllegalArgumentException("expireInSecond must be greater than 300"); + } + if (StringUtils.isBlank(this.kid)) { + throw new IllegalArgumentException("kid must be set"); + } + JwtTokenAttributes jwtTokenAttributes = new JwtTokenAttributes(); + jwtTokenAttributes.kid = this.kid; + jwtTokenAttributes.scope = this.scope; + jwtTokenAttributes.eventId = this.eventId; + jwtTokenAttributes.sub = this.sub; + jwtTokenAttributes.email = this.email; + jwtTokenAttributes.expireInSecond = this.expireInSecond; + jwtTokenAttributes.clientId = this.clientId; + jwtTokenAttributes.iss = this.iss; + jwtTokenAttributes.tokenUse = this.tokenUse; + jwtTokenAttributes.username = this.username; + return jwtTokenAttributes; + } + } +} diff --git a/src/main/java/org/clevercastle/authforge/token/jwt/JwtTokenService.java b/core/src/main/java/org/clevercastle/authforge/core/token/jwt/JwtTokenService.java similarity index 88% rename from src/main/java/org/clevercastle/authforge/token/jwt/JwtTokenService.java rename to core/src/main/java/org/clevercastle/authforge/core/token/jwt/JwtTokenService.java index 40e9fc2..a961ca3 100644 --- a/src/main/java/org/clevercastle/authforge/token/jwt/JwtTokenService.java +++ b/core/src/main/java/org/clevercastle/authforge/core/token/jwt/JwtTokenService.java @@ -1,14 +1,14 @@ -package org.clevercastle.authforge.token.jwt; +package org.clevercastle.authforge.core.token.jwt; import com.auth0.jwt.algorithms.Algorithm; -import org.clevercastle.authforge.exception.CastleException; -import org.clevercastle.authforge.Config; -import org.clevercastle.authforge.TokenHolder; -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; +import org.clevercastle.authforge.core.Config; +import org.clevercastle.authforge.core.TokenHolder; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.token.TokenService; +import org.clevercastle.authforge.core.util.JsonUtil; +import org.clevercastle.authforge.core.util.TimeUtils; import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; diff --git a/core/src/main/java/org/clevercastle/authforge/core/totp/RequestTotpResponse.java b/core/src/main/java/org/clevercastle/authforge/core/totp/RequestTotpResponse.java new file mode 100644 index 0000000..ea13c93 --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/totp/RequestTotpResponse.java @@ -0,0 +1,40 @@ +package org.clevercastle.authforge.core.totp; + +public class RequestTotpResponse { + private String sessionId; + private String secret; + private String qrCodeUri; // QR码URI + private String manualEntryKey; // 手动输入密钥(与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; + } + + public String getQrCodeUri() { + return qrCodeUri; + } + + public void setQrCodeUri(String qrCodeUri) { + this.qrCodeUri = qrCodeUri; + } + + public String getManualEntryKey() { + return manualEntryKey; + } + + public void setManualEntryKey(String manualEntryKey) { + this.manualEntryKey = manualEntryKey; + } +} diff --git a/src/main/java/org/clevercastle/authforge/totp/SetupTotpRequest.java b/core/src/main/java/org/clevercastle/authforge/core/totp/SetupTotpRequest.java similarity index 93% rename from src/main/java/org/clevercastle/authforge/totp/SetupTotpRequest.java rename to core/src/main/java/org/clevercastle/authforge/core/totp/SetupTotpRequest.java index 9a54383..d9a09d3 100644 --- a/src/main/java/org/clevercastle/authforge/totp/SetupTotpRequest.java +++ b/core/src/main/java/org/clevercastle/authforge/core/totp/SetupTotpRequest.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.totp; +package org.clevercastle.authforge.core.totp; import java.util.List; diff --git a/src/main/java/org/clevercastle/authforge/totp/SetupTotpVerificationCode.java b/core/src/main/java/org/clevercastle/authforge/core/totp/SetupTotpVerificationCode.java similarity index 90% rename from src/main/java/org/clevercastle/authforge/totp/SetupTotpVerificationCode.java rename to core/src/main/java/org/clevercastle/authforge/core/totp/SetupTotpVerificationCode.java index 575ac12..b687649 100644 --- a/src/main/java/org/clevercastle/authforge/totp/SetupTotpVerificationCode.java +++ b/core/src/main/java/org/clevercastle/authforge/core/totp/SetupTotpVerificationCode.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.totp; +package org.clevercastle.authforge.core.totp; import java.time.OffsetDateTime; diff --git a/core/src/main/java/org/clevercastle/authforge/core/totp/TotpUtil.java b/core/src/main/java/org/clevercastle/authforge/core/totp/TotpUtil.java new file mode 100644 index 0000000..afce1ea --- /dev/null +++ b/core/src/main/java/org/clevercastle/authforge/core/totp/TotpUtil.java @@ -0,0 +1,155 @@ +package org.clevercastle.authforge.core.totp; + +import org.apache.commons.codec.binary.Base32; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +public class TotpUtil { + + static Base32 base32 = new Base32(); + static Mac mac; + + // 默认时间窗口(秒) + private static final int TIME_STEP = 30; + // 允许的时间偏移窗口数量(前后各1个窗口,共3个窗口) + private static final int TIME_WINDOW = 1; + + static { + try { + mac = Mac.getInstance("HmacSHA1"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * 生成随机的Base32编码密钥 + * + * @return Base32编码的密钥 + */ + public static String generateSecret() { + SecureRandom random = new SecureRandom(); + byte[] bytes = new byte[20]; // 160 bits + random.nextBytes(bytes); + return base32.encodeAsString(bytes); + } + + /** + * 生成TOTP验证码 + * + * @param timeSeconds 时间戳(秒) + * @param secret Base32编码的密钥 + * @return 6位数字验证码 + */ + public static String generateTOTP(long timeSeconds, String secret) throws GeneralSecurityException, NoSuchAlgorithmException { + // 1. 获取当前时间(秒) + + // 2. 时间步长(默认30秒为一个窗口) + long timeStep = timeSeconds / 30; + + // 3. Base32解码Secret密钥 + byte[] keyBytes = base32.decode(secret); + // 4. 将时间步转换为8字节数组(大端格式) + ByteBuffer buffer = ByteBuffer.allocate(8); + buffer.putLong(timeStep); + byte[] timeBytes = buffer.array(); + + // 5. 使用HMAC-SHA1计算HMAC值 + SecretKeySpec signKey = new SecretKeySpec(keyBytes, "HmacSHA1"); + mac.init(signKey); + byte[] hash = mac.doFinal(timeBytes); + // 6. 动态截断(Dynamic Truncation) + int offset = hash[hash.length - 1] & 0x0F; // 最后一个字节的低4位作为偏移量 + int binary = ((hash[offset] & 0x7F) << 24) | // 取第一个字节的低7位 + ((hash[offset + 1] & 0xFF) << 16) | ((hash[offset + 2] & 0xFF) << 8) | (hash[offset + 3] & 0xFF); + // 7. 取模得到6位数字验证码 + int otp = binary % 1000000; + // 格式化为6位数字(不足前面补0) + return String.format("%06d", otp); + } + + /** + * 验证TOTP验证码 + * + * @param code 用户输入的6位验证码 + * @param secret Base32编码的密钥 + * @return 验证是否成功 + */ + public static boolean verifyTOTP(String code, String secret) { + if (code == null || code.length() != 6) { + return false; + } + + long currentTimeSeconds = System.currentTimeMillis() / 1000; + + // 检查当前时间窗口和前后TIME_WINDOW个时间窗口 + for (int i = -TIME_WINDOW; i <= TIME_WINDOW; i++) { + long timeSeconds = currentTimeSeconds + (i * TIME_STEP); + try { + String expectedCode = generateTOTP(timeSeconds, secret); + if (code.equals(expectedCode)) { + return true; + } + } catch (Exception e) { + // 忽略异常,继续检查下一个时间窗口 + continue; + } + } + + return false; + } + + /** + * 验证TOTP验证码(指定时间) + * + * @param code 用户输入的6位验证码 + * @param secret Base32编码的密钥 + * @param timeSeconds 指定的时间戳(秒) + * @return 验证是否成功 + */ + public static boolean verifyTOTPAtTime(String code, String secret, long timeSeconds) { + if (code == null || code.length() != 6) { + return false; + } + + try { + String expectedCode = generateTOTP(timeSeconds, secret); + return code.equals(expectedCode); + } catch (Exception e) { + return false; + } + } + + /** + * 生成QR码URI + * + * @param secret Base32编码的密钥 + * @param account 账户名称 + * @param issuer 发行者名称 + * @return QR码URI + */ + public static String generateQRCodeUri(String secret, String account, String issuer) { + return String.format("otpauth://totp/%s?secret=%s&issuer=%s&algorithm=SHA1&digits=6&period=30", account, secret, issuer); + } + + public static void main(String[] args) { + // 示例:设定一个Base32编码的密钥,此处使用一个示例密钥 + String secret = "JBSWY3DPEHPK3PXP"; // 你可以通过随机生成函数获得密钥 + long timeSeconds = System.currentTimeMillis() / 1000; + try { + long t1 = System.currentTimeMillis(); + for (int i = 0; i < 100000; i++) { + generateTOTP(timeSeconds, secret); + } + long t2 = System.currentTimeMillis(); + System.out.println("TOTP: " + (t2 - t1)); + } catch (GeneralSecurityException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/org/clevercastle/authforge/util/CodeUtil.java b/core/src/main/java/org/clevercastle/authforge/core/util/CodeUtil.java similarity index 94% rename from src/main/java/org/clevercastle/authforge/util/CodeUtil.java rename to core/src/main/java/org/clevercastle/authforge/core/util/CodeUtil.java index 3c51ddc..6d4a125 100644 --- a/src/main/java/org/clevercastle/authforge/util/CodeUtil.java +++ b/core/src/main/java/org/clevercastle/authforge/core/util/CodeUtil.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.util; +package org.clevercastle.authforge.core.util; import java.util.Random; diff --git a/src/main/java/org/clevercastle/authforge/util/CryptoUtil.java b/core/src/main/java/org/clevercastle/authforge/core/util/CryptoUtil.java similarity index 98% rename from src/main/java/org/clevercastle/authforge/util/CryptoUtil.java rename to core/src/main/java/org/clevercastle/authforge/core/util/CryptoUtil.java index e020e81..421b6e7 100644 --- a/src/main/java/org/clevercastle/authforge/util/CryptoUtil.java +++ b/core/src/main/java/org/clevercastle/authforge/core/util/CryptoUtil.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.util; +package org.clevercastle.authforge.core.util; -import org.clevercastle.authforge.exception.CastleException; +import org.clevercastle.authforge.core.exception.CastleException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/org/clevercastle/authforge/util/HashUtil.java b/core/src/main/java/org/clevercastle/authforge/core/util/HashUtil.java similarity index 86% rename from src/main/java/org/clevercastle/authforge/util/HashUtil.java rename to core/src/main/java/org/clevercastle/authforge/core/util/HashUtil.java index ba28375..331d36d 100644 --- a/src/main/java/org/clevercastle/authforge/util/HashUtil.java +++ b/core/src/main/java/org/clevercastle/authforge/core/util/HashUtil.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.util; +package org.clevercastle.authforge.core.util; public class HashUtil { public static String hashPassword(String password) { diff --git a/src/main/java/org/clevercastle/authforge/util/Hex.java b/core/src/main/java/org/clevercastle/authforge/core/util/Hex.java similarity index 96% rename from src/main/java/org/clevercastle/authforge/util/Hex.java rename to core/src/main/java/org/clevercastle/authforge/core/util/Hex.java index 9593b7d..e63c89e 100644 --- a/src/main/java/org/clevercastle/authforge/util/Hex.java +++ b/core/src/main/java/org/clevercastle/authforge/core/util/Hex.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.util; +package org.clevercastle.authforge.core.util; public final class Hex { diff --git a/src/main/java/org/clevercastle/authforge/util/IdUtil.java b/core/src/main/java/org/clevercastle/authforge/core/util/IdUtil.java similarity index 70% rename from src/main/java/org/clevercastle/authforge/util/IdUtil.java rename to core/src/main/java/org/clevercastle/authforge/core/util/IdUtil.java index f05bbe9..b2b5ae7 100644 --- a/src/main/java/org/clevercastle/authforge/util/IdUtil.java +++ b/core/src/main/java/org/clevercastle/authforge/core/util/IdUtil.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.util; +package org.clevercastle.authforge.core.util; -import org.clevercastle.authforge.model.ResourceType; +import org.clevercastle.authforge.core.model.ResourceType; import java.util.UUID; diff --git a/src/main/java/org/clevercastle/authforge/util/JsonUtil.java b/core/src/main/java/org/clevercastle/authforge/core/util/JsonUtil.java similarity index 97% rename from src/main/java/org/clevercastle/authforge/util/JsonUtil.java rename to core/src/main/java/org/clevercastle/authforge/core/util/JsonUtil.java index 15145f2..c975b6e 100644 --- a/src/main/java/org/clevercastle/authforge/util/JsonUtil.java +++ b/core/src/main/java/org/clevercastle/authforge/core/util/JsonUtil.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.util; +package org.clevercastle.authforge.core.util; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; diff --git a/src/main/java/org/clevercastle/authforge/util/TimeUtils.java b/core/src/main/java/org/clevercastle/authforge/core/util/TimeUtils.java similarity index 99% rename from src/main/java/org/clevercastle/authforge/util/TimeUtils.java rename to core/src/main/java/org/clevercastle/authforge/core/util/TimeUtils.java index daa1bdc..5e5fddd 100644 --- a/src/main/java/org/clevercastle/authforge/util/TimeUtils.java +++ b/core/src/main/java/org/clevercastle/authforge/core/util/TimeUtils.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.util; +package org.clevercastle.authforge.core.util; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; diff --git a/src/test/java/org/clevercastle/authforge/oauth2/github/GithubOauth2ExchangeServiceTest.java b/core/src/test/java/org/clevercastle/authforge/core/oauth2/github/GithubOauth2ExchangeServiceTest.java similarity index 86% rename from src/test/java/org/clevercastle/authforge/oauth2/github/GithubOauth2ExchangeServiceTest.java rename to core/src/test/java/org/clevercastle/authforge/core/oauth2/github/GithubOauth2ExchangeServiceTest.java index 19e5280..3c0499b 100644 --- a/src/test/java/org/clevercastle/authforge/oauth2/github/GithubOauth2ExchangeServiceTest.java +++ b/core/src/test/java/org/clevercastle/authforge/core/oauth2/github/GithubOauth2ExchangeServiceTest.java @@ -1,15 +1,15 @@ -package org.clevercastle.authforge.oauth2.github; +package org.clevercastle.authforge.core.oauth2.github; import com.nimbusds.oauth2.sdk.ParseException; import com.nimbusds.oauth2.sdk.TokenResponse; import com.nimbusds.oauth2.sdk.http.HTTPRequest; import com.nimbusds.oauth2.sdk.http.HTTPResponse; import com.nimbusds.oauth2.sdk.token.AccessTokenType; -import org.clevercastle.authforge.exception.CastleException; -import org.clevercastle.authforge.http.HttpRequest; -import org.clevercastle.authforge.http.HttpResponse; -import org.clevercastle.authforge.http.IHttpClient; -import org.clevercastle.authforge.oauth2.Oauth2ClientConfig; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.http.HttpRequest; +import org.clevercastle.authforge.core.http.HttpResponse; +import org.clevercastle.authforge.core.http.IHttpClient; +import org.clevercastle.authforge.core.oauth2.Oauth2ClientConfig; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/core/src/test/java/org/clevercastle/authforge/core/oauth2/oidc/OidcExchangeServiceTest.java b/core/src/test/java/org/clevercastle/authforge/core/oauth2/oidc/OidcExchangeServiceTest.java new file mode 100644 index 0000000..4cb9536 --- /dev/null +++ b/core/src/test/java/org/clevercastle/authforge/core/oauth2/oidc/OidcExchangeServiceTest.java @@ -0,0 +1,4 @@ +package org.clevercastle.authforge.core.oauth2.oidc; + +public class OidcExchangeServiceTest { +} diff --git a/core/src/test/java/org/clevercastle/authforge/core/totp/TotpUtilTest.java b/core/src/test/java/org/clevercastle/authforge/core/totp/TotpUtilTest.java new file mode 100644 index 0000000..e34f698 --- /dev/null +++ b/core/src/test/java/org/clevercastle/authforge/core/totp/TotpUtilTest.java @@ -0,0 +1,160 @@ +package org.clevercastle.authforge.core.totp; + +import org.junit.jupiter.api.Test; + +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * TOTP 工具类测试 + */ +public class TotpUtilTest { + + @Test + public void testGenerateSecret() { + // 测试密钥生成 + String secret1 = TotpUtil.generateSecret(); + String secret2 = TotpUtil.generateSecret(); + + assertNotNull(secret1); + assertNotNull(secret2); + assertNotEquals(secret1, secret2); // 每次生成的密钥应该不同 + assertTrue(secret1.length() > 0); + + // Base32编码字符集测试 + assertTrue(secret1.matches("[A-Z2-7]+")); + } + + @Test + public void testGenerateTOTP() throws GeneralSecurityException, NoSuchAlgorithmException { + String secret = "JBSWY3DPEHPK3PXP"; // 测试用密钥 + long timeSeconds = 1234567890L; // 固定时间戳 + + String totp = TotpUtil.generateTOTP(timeSeconds, secret); + + assertNotNull(totp); + assertEquals(6, totp.length()); // TOTP码应该是6位 + assertTrue(totp.matches("\\d{6}")); // 应该全是数字 + } + + @Test + public void testVerifyTOTP() throws GeneralSecurityException, NoSuchAlgorithmException { + String secret = TotpUtil.generateSecret(); + long currentTime = System.currentTimeMillis() / 1000; + + // 生成当前时间的TOTP码 + String validCode = TotpUtil.generateTOTP(currentTime, secret); + + // 验证应该成功 + assertTrue(TotpUtil.verifyTOTP(validCode, secret)); + + // 错误的代码应该验证失败 + assertFalse(TotpUtil.verifyTOTP("000000", secret)); + assertFalse(TotpUtil.verifyTOTP("123456", secret)); + + // 空值或格式错误的代码应该验证失败 + assertFalse(TotpUtil.verifyTOTP(null, secret)); + assertFalse(TotpUtil.verifyTOTP("", secret)); + assertFalse(TotpUtil.verifyTOTP("12345", secret)); // 长度不对 + assertFalse(TotpUtil.verifyTOTP("1234567", secret)); // 长度不对 + assertFalse(TotpUtil.verifyTOTP("abcdef", secret)); // 不是数字 + } + + @Test + public void testVerifyTOTPAtTime() throws GeneralSecurityException, NoSuchAlgorithmException { + String secret = TotpUtil.generateSecret(); + long fixedTime = 1234567890L; + + // 生成特定时间的TOTP码 + String codeAtTime = TotpUtil.generateTOTP(fixedTime, secret); + + // 使用相同时间验证应该成功 + assertTrue(TotpUtil.verifyTOTPAtTime(codeAtTime, secret, fixedTime)); + + // 使用不同时间验证应该失败 + assertFalse(TotpUtil.verifyTOTPAtTime(codeAtTime, secret, fixedTime + 60)); + } + + @Test + public void testTimeWindowTolerance() throws GeneralSecurityException, NoSuchAlgorithmException { + String secret = TotpUtil.generateSecret(); + long currentTime = System.currentTimeMillis() / 1000; + + // 生成当前时间的TOTP码 + String currentCode = TotpUtil.generateTOTP(currentTime, secret); + + // 当前时间应该验证成功 + assertTrue(TotpUtil.verifyTOTP(currentCode, secret)); + + // 测试时间窗口容忍度 + // 前一个时间窗口的代码 + String prevCode = TotpUtil.generateTOTP(currentTime - 30, secret); + // 后一个时间窗口的代码 + String nextCode = TotpUtil.generateTOTP(currentTime + 30, secret); + + // 注意:这个测试可能因为实际运行时间与生成时间的差异而不稳定 + // 在实际项目中,应该使用mock时间或更精确的时间控制 + } + + @Test + public void testGenerateQRCodeUri() { + String secret = "JBSWY3DPEHPK3PXP"; + String account = "user@example.com"; + String issuer = "AuthForge"; + + String uri = TotpUtil.generateQRCodeUri(secret, account, issuer); + + assertNotNull(uri); + assertTrue(uri.startsWith("otpauth://totp/")); + assertTrue(uri.contains("secret=" + secret)); + assertTrue(uri.contains("issuer=" + issuer)); + assertTrue(uri.contains(account)); + assertTrue(uri.contains("algorithm=SHA1")); + assertTrue(uri.contains("digits=6")); + assertTrue(uri.contains("period=30")); + } + + @Test + public void testConsistency() throws GeneralSecurityException, NoSuchAlgorithmException { + // 测试相同输入产生相同输出 + String secret = "JBSWY3DPEHPK3PXP"; + long timeSeconds = 1234567890L; + + String totp1 = TotpUtil.generateTOTP(timeSeconds, secret); + String totp2 = TotpUtil.generateTOTP(timeSeconds, secret); + + assertEquals(totp1, totp2); // 相同输入应该产生相同输出 + } + + @Test + public void testDifferentSecretsDifferentCodes() throws GeneralSecurityException, NoSuchAlgorithmException { + // 测试不同密钥在相同时间产生不同代码 + String secret1 = "JBSWY3DPEHPK3PXP"; + String secret2 = "JBSWY3DPEHPK3PXQ"; + long timeSeconds = 1234567890L; + + String totp1 = TotpUtil.generateTOTP(timeSeconds, secret1); + String totp2 = TotpUtil.generateTOTP(timeSeconds, secret2); + + assertNotEquals(totp1, totp2); // 不同密钥应该产生不同代码 + } + + @Test + public void testDifferentTimesDifferentCodes() throws GeneralSecurityException, NoSuchAlgorithmException { + // 测试相同密钥在不同时间产生不同代码 + String secret = "JBSWY3DPEHPK3PXP"; + long time1 = 1234567890L; + long time2 = 1234567890L + 60; // 不同时间窗口 + + String totp1 = TotpUtil.generateTOTP(time1, secret); + String totp2 = TotpUtil.generateTOTP(time2, secret); + + assertNotEquals(totp1, totp2); // 不同时间应该产生不同代码 + } +} diff --git a/src/test/java/org/clevercastle/authforge/util/CryptoUtilTest.java b/core/src/test/java/org/clevercastle/authforge/core/util/CryptoUtilTest.java similarity index 98% rename from src/test/java/org/clevercastle/authforge/util/CryptoUtilTest.java rename to core/src/test/java/org/clevercastle/authforge/core/util/CryptoUtilTest.java index 741a956..9c4e052 100644 --- a/src/test/java/org/clevercastle/authforge/util/CryptoUtilTest.java +++ b/core/src/test/java/org/clevercastle/authforge/core/util/CryptoUtilTest.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.util; +package org.clevercastle.authforge.core.util; -import org.clevercastle.authforge.exception.CastleException; +import org.clevercastle.authforge.core.exception.CastleException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; diff --git a/core/src/test/java/org/clevercastle/helper/login/Base.java b/core/src/test/java/org/clevercastle/helper/login/Base.java new file mode 100644 index 0000000..42433af --- /dev/null +++ b/core/src/test/java/org/clevercastle/helper/login/Base.java @@ -0,0 +1,4 @@ +package org.clevercastle.helper.login; + +public class Base { +} diff --git a/docs/mfa-usage-guide.md b/docs/mfa-usage-guide.md new file mode 100644 index 0000000..3aa7784 --- /dev/null +++ b/docs/mfa-usage-guide.md @@ -0,0 +1,242 @@ +# Auth-Forge MFA 功能使用指南 + +## 概览 + +Auth-Forge 提供了完整的多因子认证(MFA)功能,兼容 Auth0 的 MFA 接口设计。主要支持以下功能: + +- **TOTP (Time-based One-Time Password)**: 基于时间的一次性密码 +- **MFA Challenge**: 挑战-响应验证流程 +- **MFA Factor Management**: MFA 因子的管理 + +## 功能特性 + +### 1. TOTP 支持 + +- 生成 Base32 编码的密钥 +- 支持标准的 30 秒时间窗口 +- 提供 QR 码 URI 用于认证器应用 +- 支持时间偏移容忍(±1 个时间窗口) + +### 2. MFA 挑战流程 + +- 创建 MFA 挑战会话 +- 验证用户提供的 MFA 代码 +- 支持挑战会话过期管理 + +### 3. MFA 因子管理 + +- 列出用户的所有 MFA 因子 +- 删除特定的 MFA 因子 +- 查看因子的使用历史 + +## API 使用示例 + +### 1. TOTP 设置流程 + +#### 第一步:请求 TOTP 设置 + +```java +// 获取 TOTP 设置所需的密钥和 QR 码 +RequestTotpResponse response = userService.requestTotp(user); + +System.out.println("Session ID: " + response.getSessionId()); +System.out.println("Secret: " + response.getSecret()); +System.out.println("QR Code URI: " + response.getQrCodeUri()); +``` + +#### 第二步:用户扫描 QR 码并设置认证器 + +用户需要: + +1. 使用认证器应用(如 Google Authenticator、Authy)扫描 QR 码 +2. 或者手动输入密钥到认证器应用中 +3. 从认证器应用中获取 6 位数字验证码 + +#### 第三步:完成 TOTP 设置 + +```java +SetupTotpRequest request = new SetupTotpRequest(); +request.setSessionId(response.getSessionId()); +request.setName("My Phone Authenticator"); + +// 添加用户输入的验证码 +SetupTotpVerificationCode code = new SetupTotpVerificationCode(); +code.setCode("123456"); // 用户从认证器应用获取的代码 +code.setInputTime(OffsetDateTime.now()); +request.setCodes(Arrays.asList(code)); + +// 完成设置 +userService.setupTotp(user, request); +``` + +### 2. MFA 挑战验证流程 + +#### 创建 MFA 挑战 + +```java +// 为用户创建 TOTP 挑战 +MfaChallengeResponse challenge = userService.createMfaChallenge( + user, "totp", factorId); + +System.out.println("Challenge ID: " + challenge.getChallengeId()); +System.out.println("Expires at: " + challenge.getExpiresAt()); +``` + +#### 验证 MFA 挑战 + +```java +// 用户输入认证器应用中的代码进行验证 +boolean verified = userService.verifyMfaChallenge( + challengeId, "654321", null); + +if (verified) { + System.out.println("MFA verification successful!"); +} else { + System.out.println("MFA verification failed!"); +} +``` + +### 3. 直接 TOTP 验证(简化流程) + +```java +// 直接验证用户的 TOTP 代码,无需创建挑战 +boolean verified = userService.verifyTotpCode(userId, "123456"); +``` + +### 4. MFA 因子管理 + +#### 列出用户的 MFA 因子 + +```java +List factors = userService.listMfaFactors(userId); + +for (MfaFactorResponse factor : factors) { + System.out.println("Factor: " + factor.getName() + + " (" + factor.getType() + ")"); + System.out.println("Created: " + factor.getCreatedAt()); + System.out.println("Last used: " + factor.getLastUsedAt()); +} +``` + +#### 删除 MFA 因子 + +```java +userService.deleteMfaFactor(userId, factorId); +``` + +## 与 Auth0 MFA 的兼容性 + +本实现参考了 Auth0 的 MFA API 设计,主要兼容以下 Auth0 功能: + +### 1. TOTP 因子管理 + +- 类似于 Auth0 的 `/api/v2/users/{id}/authenticators` 接口 +- 支持 TOTP 因子的创建、列表和删除 + +### 2. MFA 挑战流程 + +- 类似于 Auth0 的 MFA Challenge API +- 支持挑战的创建和验证 + +### 3. QR 码生成 + +- 生成标准的 `otpauth://` URI +- 兼容主流认证器应用 + +## 安全考虑 + +### 1. 密钥安全 + +- 使用 `SecureRandom` 生成密钥 +- 密钥采用 Base32 编码存储 +- 临时密钥使用缓存存储并设置过期时间 + +### 2. 时间同步 + +- 支持 ±1 个时间窗口的容忍度 +- 防止时钟偏移导致的验证失败 + +### 3. 重放攻击防护 + +- 可以扩展添加使用过的 TOTP 代码记录 +- 挑战会话具有过期时间 + +### 4. 暴力破解防护 + +- 可以添加验证失败次数限制 +- 可以添加账户锁定机制 + +## 配置选项 + +### 时间窗口配置 + +```java +// 在 TotpUtil 中可以调整以下参数: +private static final int TIME_STEP = 30; // 时间步长(秒) +private static final int TIME_WINDOW = 1; // 允许的时间窗口偏移 +``` + +### 挑战会话过期时间 + +```java +// 在 createMfaChallenge 方法中可以调整: +cacheService.set("mfa_challenge_" + challengeId, factorId, 300); // 5分钟过期 +``` + +## 扩展功能建议 + +### 1. 备用代码支持 + +可以扩展支持一次性备用代码,类似于 Auth0 的 Recovery Codes。 + +### 2. SMS OOB 支持 + +可以扩展支持短信验证,参考 Auth0 的 OOB (Out-of-Band) 验证。 + +### 3. 邮件 OOB 支持 + +可以扩展支持邮件验证。 + +### 4. 推送通知 + +可以集成推送通知服务,类似于 Auth0 Guardian。 + +## 错误处理 + +### 常见错误及处理 + +```java +try { + userService.setupTotp(user, request); +} catch (CastleException e) { + if (e.getMessage().contains("Invalid verification code")) { + // 处理验证码错误 + } else if (e.getMessage().contains("session expired")) { + // 处理会话过期 + } else if (e.getMessage().contains("already configured")) { + // 处理重复配置 + } +} +``` + +## 测试建议 + +### 1. 单元测试 + +- 测试 TOTP 生成和验证逻辑 +- 测试时间窗口容忍度 +- 测试密钥生成的随机性 + +### 2. 集成测试 + +- 测试完整的 MFA 设置流程 +- 测试挑战验证流程 +- 测试因子管理功能 + +### 3. 安全测试 + +- 测试重放攻击防护 +- 测试暴力破解防护 +- 测试会话过期处理 + +这个 MFA 实现为你的认证系统提供了强大的多因子认证功能,同时保持了与 Auth0 接口的兼容性,便于未来的迁移或集成。 diff --git a/docs/mfa.md b/docs/mfa.md index f602307..9b21146 100644 --- a/docs/mfa.md +++ b/docs/mfa.md @@ -1,18 +1,102 @@ -# Mfa Flow +# MFA Flow + +## TOTP Setup Flow + +```mermaid +flowchart TD + A[User requests TOTP setup] --> B[Generate secret key] + B --> C[Store secret in cache with session ID] + C --> D[Generate QR code URI] + D --> E[Return session ID, secret, and QR code to user] + E --> F[User scans QR code with authenticator app] + F --> G[User enters verification code from app] + G --> H[Client sends session ID and verification code] + H --> I[Server retrieves secret from cache using session ID] + I --> J[Server verifies TOTP code using secret] + J --> K{Verification successful?} + K -- Yes --> L[Save secret to database] + K -- No --> M[Return error] + L --> N[Clear cache] + N --> O[TOTP setup complete] + M --> P[User can retry with new code] +``` + +## MFA Challenge Flow + +```mermaid +flowchart TD + A[User login with username/password] --> B{User has MFA enabled?} + B -- No --> C[Login successful] + B -- Yes --> D[Create MFA challenge] + D --> E[Store challenge session with expiration] + E --> F[Return challenge ID to client] + F --> G[User opens authenticator app] + G --> H[User enters TOTP code] + H --> I[Client sends challenge ID and TOTP code] + I --> J[Server validates challenge session] + J --> K{Challenge valid and not expired?} + K -- No --> L[Return challenge error] + K -- Yes --> M[Retrieve user's TOTP secret] + M --> N[Verify TOTP code against secret] + N --> O{TOTP code valid?} + O -- No --> P[Return verification error] + O -- Yes --> Q[Clear challenge session] + Q --> R[Complete login process] + R --> S[Return access token] +``` + +## MFA Factor Management Flow + +```mermaid +flowchart TD + A[User requests MFA factors] --> B[Retrieve user's MFA factors from database] + B --> C[Return list of factors with metadata] + C --> D[User views factors list] + D --> E{User action?} + E -- Add TOTP --> F[Start TOTP setup flow] + E -- Delete factor --> G[Delete factor from database] + E -- Test factor --> H[Create test challenge] + F --> I[TOTP setup flow] + G --> J[Update factors list] + H --> K[MFA challenge flow] +``` + +## Security Considerations + +### Time Window Tolerance + +- TOTP codes are valid for ±1 time window (30 seconds each) +- Prevents clock drift issues between server and client + +### Challenge Session Management + +- Challenge sessions expire after 5 minutes +- Prevents replay attacks and session hijacking + +### Secret Key Security + +- Secrets generated using SecureRandom +- Temporary secrets stored in cache with expiration +- Persistent secrets encrypted in database + +### Rate Limiting (Recommended) + +- Limit TOTP verification attempts +- Implement account lockout after failed attempts +- Add delays between verification attempts + +# Mfa Challenge 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 + B -- Yes --> C{Server: decide whether need challenge} + C -- Yes --> D[Server: Return the challenge to the client, response should include all possible challenge response solutions] + C -- No --> Y[Server: Return the token to the user] + D --> E[Customer choose on solution and answer the challenge] + E --> F{Server: is the response correct} + F -- No --> E + F -- Yes --> Y + ``` \ No newline at end of file diff --git a/examples/spring-boot-example/build.gradle.kts b/examples/spring-boot-example/build.gradle.kts index 9128b2d..d8c64e7 100644 --- a/examples/spring-boot-example/build.gradle.kts +++ b/examples/spring-boot-example/build.gradle.kts @@ -13,12 +13,9 @@ java { } } -repositories { - mavenCentral() -} - dependencies { - implementation(rootProject) + developmentOnly("org.springframework.boot:spring-boot-devtools") + implementation(project(":core")) implementation("org.apache.httpcomponents.client5:httpclient5:5.4.2") implementation("org.apache.httpcomponents.core5:httpcore5:5.3.4") @@ -26,12 +23,12 @@ dependencies { implementation("software.amazon.awssdk:dynamodb:2.31.31") implementation("software.amazon.awssdk:dynamodb-enhanced:2.31.31") - implementation("org.apache.commons:commons-lang3:3.17.0") - implementation("com.auth0:java-jwt:4.5.0") + implementation("org.apache.commons:commons-lang3:3.18.0") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-data-jpa") -// developmentOnly("org.springframework.boot:spring-boot-devtools") - runtimeOnly("org.postgresql:postgresql:42.7.5") + runtimeOnly("org.postgresql:postgresql:42.7.7") + implementation("com.auth0:auth0:2.24.0") + implementation("com.auth0:java-jwt:4.5.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/AuthController.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/AuthController.java similarity index 89% rename from examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/AuthController.java rename to examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/AuthController.java index e813433..2a2cceb 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/core/examples/springboot/springbootexample/AuthController.java @@ -1,19 +1,19 @@ -package org.clevercastle.authforge.examples.springboot.springbootexample; +package org.clevercastle.authforge.core.examples.springboot.springbootexample; import com.auth0.jwt.JWT; 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.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; +import org.clevercastle.authforge.core.UserRegisterRequest; +import org.clevercastle.authforge.core.UserService; +import org.clevercastle.authforge.core.UserWithToken; +import org.clevercastle.authforge.core.dto.OneTimePasswordDto; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.oauth2.Oauth2ClientConfig; +import org.clevercastle.authforge.core.oauth2.github.GithubOauth2ExchangeService; +import org.clevercastle.authforge.core.oauth2.oidc.OidcExchangeService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; 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/core/examples/springboot/springbootexample/Beans.java similarity index 76% rename from examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/Beans.java rename to examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/Beans.java index 0c60204..3d544a3 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/core/examples/springboot/springbootexample/Beans.java @@ -1,22 +1,22 @@ -package org.clevercastle.authforge.examples.springboot.springbootexample; +package org.clevercastle.authforge.core.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; -import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserRepositoryImpl; -import org.clevercastle.authforge.token.TokenService; -import org.clevercastle.authforge.token.jwt.JwtTokenService; -import org.clevercastle.authforge.code.DummyCodeSender; +import org.clevercastle.authforge.core.DummyCacheServiceImpl; +import org.clevercastle.authforge.core.Config; +import org.clevercastle.authforge.core.UserService; +import org.clevercastle.authforge.core.UserServiceImpl; +import org.clevercastle.authforge.core.repository.UserRepository; +import org.clevercastle.authforge.core.repository.dynamodb.DynamodbUserRepositoryImpl; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaChallengeSessionRepository; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaOneTimePasswordRepository; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserHmacSecretRepository; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserLoginItemRepository; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserModelRepository; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserRefreshTokenMappingRepository; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserRepositoryImpl; +import org.clevercastle.authforge.core.token.TokenService; +import org.clevercastle.authforge.core.token.jwt.JwtTokenService; +import org.clevercastle.authforge.core.code.DummyCodeSender; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/ChallengeSessionRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/ChallengeSessionRepositoryAdapter.java new file mode 100644 index 0000000..2984fd8 --- /dev/null +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/ChallengeSessionRepositoryAdapter.java @@ -0,0 +1,8 @@ +package org.clevercastle.authforge.core.examples.springboot.springbootexample; + +import org.clevercastle.authforge.core.model.ChallengeSession; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaChallengeSessionRepository; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChallengeSessionRepositoryAdapter extends RdsJpaChallengeSessionRepository, JpaRepository { +} diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/HttpClientImpl.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/HttpClientImpl.java similarity index 85% rename from examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/HttpClientImpl.java rename to examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/HttpClientImpl.java index aff7d69..0605bab 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/HttpClientImpl.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/HttpClientImpl.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.examples.springboot.springbootexample; +package org.clevercastle.authforge.core.examples.springboot.springbootexample; import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; import org.apache.hc.client5.http.config.RequestConfig; @@ -7,10 +7,10 @@ import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.util.Timeout; -import org.clevercastle.authforge.exception.CastleException; -import org.clevercastle.authforge.http.HttpRequest; -import org.clevercastle.authforge.http.HttpResponse; -import org.clevercastle.authforge.http.IHttpClient; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.http.HttpRequest; +import org.clevercastle.authforge.core.http.HttpResponse; +import org.clevercastle.authforge.core.http.IHttpClient; import java.io.IOException; import java.net.URI; diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/OneTimePasswordRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/OneTimePasswordRepositoryAdapter.java new file mode 100644 index 0000000..b9448d5 --- /dev/null +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/OneTimePasswordRepositoryAdapter.java @@ -0,0 +1,9 @@ +package org.clevercastle.authforge.core.examples.springboot.springbootexample; + +import org.clevercastle.authforge.core.model.OneTimePassword; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaOneTimePasswordId; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaOneTimePasswordRepository; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface OneTimePasswordRepositoryAdapter extends RdsJpaOneTimePasswordRepository, JpaRepository { +} diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/RefreshTokenRequest.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/RefreshTokenRequest.java similarity index 75% rename from examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/RefreshTokenRequest.java rename to examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/RefreshTokenRequest.java index 50c3356..75af6af 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/RefreshTokenRequest.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/RefreshTokenRequest.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.examples.springboot.springbootexample; +package org.clevercastle.authforge.core.examples.springboot.springbootexample; public class RefreshTokenRequest { private String refreshToken; diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/RegisterRequest.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/RegisterRequest.java similarity index 82% rename from examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/RegisterRequest.java rename to examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/RegisterRequest.java index 3e2a9b7..1eade74 100644 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/RegisterRequest.java +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/RegisterRequest.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.examples.springboot.springbootexample; +package org.clevercastle.authforge.core.examples.springboot.springbootexample; public class RegisterRequest { private String email; 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/core/examples/springboot/springbootexample/SendOneTimeRequest.java similarity index 71% rename from examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/SendOneTimeRequest.java rename to examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/SendOneTimeRequest.java index b678839..bb1ca8d 100644 --- 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/core/examples/springboot/springbootexample/SendOneTimeRequest.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.examples.springboot.springbootexample; +package org.clevercastle.authforge.core.examples.springboot.springbootexample; public class SendOneTimeRequest { private String 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/core/examples/springboot/springbootexample/SpringBootExampleApplication.java similarity index 76% rename from examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/SpringBootExampleApplication.java rename to examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/SpringBootExampleApplication.java index dd4c668..b8a8d08 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/core/examples/springboot/springbootexample/SpringBootExampleApplication.java @@ -1,6 +1,6 @@ -package org.clevercastle.authforge.examples.springboot.springbootexample; +package org.clevercastle.authforge.core.examples.springboot.springbootexample; -import org.clevercastle.authforge.model.User; +import org.clevercastle.authforge.core.model.User; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/SsoType.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/SsoType.java new file mode 100644 index 0000000..34175dd --- /dev/null +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/SsoType.java @@ -0,0 +1,6 @@ +package org.clevercastle.authforge.core.examples.springboot.springbootexample; + +public enum SsoType { + github, + google +} 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/core/examples/springboot/springbootexample/UserController.java similarity index 68% rename from examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserController.java rename to examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserController.java index eff85fb..fdae3f2 100644 --- 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/core/examples/springboot/springbootexample/UserController.java @@ -1,10 +1,10 @@ -package org.clevercastle.authforge.examples.springboot.springbootexample; +package org.clevercastle.authforge.core.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.clevercastle.authforge.core.UserService; +import org.clevercastle.authforge.core.exception.CastleException; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.totp.RequestTotpResponse; +import org.clevercastle.authforge.core.totp.SetupTotpRequest; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserHmacSecretRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserHmacSecretRepositoryAdapter.java new file mode 100644 index 0000000..5fcebbf --- /dev/null +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserHmacSecretRepositoryAdapter.java @@ -0,0 +1,9 @@ +package org.clevercastle.authforge.core.examples.springboot.springbootexample; + +import org.clevercastle.authforge.core.model.UserHmacSecret; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserHmacSecretId; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserHmacSecretRepository; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserHmacSecretRepositoryAdapter extends RdsJpaUserHmacSecretRepository, JpaRepository { +} diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserLoginItemRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserLoginItemRepositoryAdapter.java similarity index 52% rename from examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserLoginItemRepositoryAdapter.java rename to examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserLoginItemRepositoryAdapter.java index c0a3b17..84d6c37 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/core/examples/springboot/springbootexample/UserLoginItemRepositoryAdapter.java @@ -1,7 +1,7 @@ -package org.clevercastle.authforge.examples.springboot.springbootexample; +package org.clevercastle.authforge.core.examples.springboot.springbootexample; -import org.clevercastle.authforge.model.UserLoginItem; -import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserLoginItemRepository; +import org.clevercastle.authforge.core.model.UserLoginItem; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserLoginItemRepository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; 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/core/examples/springboot/springbootexample/UserModelRepositoryAdapter.java similarity index 52% rename from examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserModelRepositoryAdapter.java rename to examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserModelRepositoryAdapter.java index 6ee6bdf..939b8a8 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/core/examples/springboot/springbootexample/UserModelRepositoryAdapter.java @@ -1,7 +1,7 @@ -package org.clevercastle.authforge.examples.springboot.springbootexample; +package org.clevercastle.authforge.core.examples.springboot.springbootexample; -import org.clevercastle.authforge.model.User; -import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserModelRepository; +import org.clevercastle.authforge.core.model.User; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserModelRepository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserRefreshTokenMappingRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserRefreshTokenMappingRepositoryAdapter.java new file mode 100644 index 0000000..656b984 --- /dev/null +++ b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/UserRefreshTokenMappingRepositoryAdapter.java @@ -0,0 +1,9 @@ +package org.clevercastle.authforge.core.examples.springboot.springbootexample; + +import org.clevercastle.authforge.core.model.UserRefreshTokenMapping; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserRefreshTokenMappingRepository; +import org.clevercastle.authforge.core.repository.rdsjpa.RdsJpaUserRefreshTokenMappingId; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRefreshTokenMappingRepositoryAdapter extends RdsJpaUserRefreshTokenMappingRepository, JpaRepository { +} diff --git a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/VerifyOneTimeRequest.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/VerifyOneTimeRequest.java similarity index 84% rename from examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/VerifyOneTimeRequest.java rename to examples/spring-boot-example/src/main/java/org/clevercastle/authforge/core/examples/springboot/springbootexample/VerifyOneTimeRequest.java index 267247d..328a9ac 100644 --- 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/core/examples/springboot/springbootexample/VerifyOneTimeRequest.java @@ -1,4 +1,4 @@ -package org.clevercastle.authforge.examples.springboot.springbootexample; +package org.clevercastle.authforge.core.examples.springboot.springbootexample; public class VerifyOneTimeRequest { private String email; 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 deleted file mode 100644 index 24da352..0000000 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/ChallengeSessionRepositoryAdapter.java +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index e180bbd..0000000 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/OneTimePasswordRepositoryAdapter.java +++ /dev/null @@ -1,9 +0,0 @@ -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/SsoType.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/SsoType.java deleted file mode 100644 index 99c6c37..0000000 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/SsoType.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.clevercastle.authforge.examples.springboot.springbootexample; - -public enum SsoType { - github, - google -} 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 deleted file mode 100644 index 41d2c7f..0000000 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserHmacSecretRepositoryAdapter.java +++ /dev/null @@ -1,11 +0,0 @@ -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/UserRefreshTokenMappingRepositoryAdapter.java b/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserRefreshTokenMappingRepositoryAdapter.java deleted file mode 100644 index 259bba7..0000000 --- a/examples/spring-boot-example/src/main/java/org/clevercastle/authforge/examples/springboot/springbootexample/UserRefreshTokenMappingRepositoryAdapter.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.clevercastle.authforge.examples.springboot.springbootexample; - -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; - -public interface UserRefreshTokenMappingRepositoryAdapter extends RdsJpaUserRefreshTokenMappingRepository, JpaRepository { -} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c14da56..9f4df1c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Mar 24 14:43:59 CST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/impls/impl-postgres/build.gradle.kts b/impls/impl-postgres/build.gradle.kts new file mode 100644 index 0000000..f5914ae --- /dev/null +++ b/impls/impl-postgres/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + runtimeOnly("org.postgresql:postgresql:42.7.7") +} \ No newline at end of file diff --git a/readme.md b/readme.md index ca8c604..6f01d3e 100644 --- a/readme.md +++ b/readme.md @@ -35,26 +35,26 @@ This system is designed to be scalable, secure, and easily integrable with exist - [ ] PostgreSQL (In progress) - [ ] MySQL -- [ ] DynamoEDB +- [ ] DynamoEDB (In progress) - [ ] MongoDB ### Authentication provider -- [ ] Email + password (Basic Authentication) (in progress) +- [x] Email + password (Basic Authentication) - [ ] Email + password (pbkdf2) -- [ ] Email + one time password +- [x] Email + one time password - [ ] Email + Passkey - [ ] Api key -- [ ] Multi-Factor authentication +- [ ] Multi-Factor authentication (In progress) ### SSO provider -- [ ] Google (in progress) -- [ ] GitHub (in progress) +- [x] Google +- [x] GitHub - [ ] Apple - [ ] Microsoft - [ ] Okta -- [ ] OAuth2 (in progress) +- [x] OIDC ### Infra diff --git a/settings.gradle.kts b/settings.gradle.kts index db29220..4448bf6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,7 @@ rootProject.name = "auth-forge" -include("examples:spring-boot-example") \ No newline at end of file +include("examples:spring-boot-example") +include("core") + +include("impls:impl-dynamodb") +include("impls:impl-postgres") \ No newline at end of file diff --git a/src/main/java/org/clevercastle/authforge/http/IHttpClient.java b/src/main/java/org/clevercastle/authforge/http/IHttpClient.java deleted file mode 100644 index ee0841c..0000000 --- a/src/main/java/org/clevercastle/authforge/http/IHttpClient.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.clevercastle.authforge.http; - -import org.clevercastle.authforge.exception.CastleException; - -public interface IHttpClient { - HttpResponse execute(HttpRequest request) throws CastleException; -} diff --git a/src/main/java/org/clevercastle/authforge/token/TokenService.java b/src/main/java/org/clevercastle/authforge/token/TokenService.java deleted file mode 100644 index 496e2a7..0000000 --- a/src/main/java/org/clevercastle/authforge/token/TokenService.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.clevercastle.authforge.token; - -import org.clevercastle.authforge.exception.CastleException; -import org.clevercastle.authforge.TokenHolder; -import org.clevercastle.authforge.model.User; -import org.clevercastle.authforge.model.UserLoginItem; - -public interface TokenService { - enum Scope { - access, - id - } - - TokenHolder generateToken(User user, UserLoginItem item) throws CastleException; -} diff --git a/src/main/java/org/clevercastle/authforge/totp/RequestTotpResponse.java b/src/main/java/org/clevercastle/authforge/totp/RequestTotpResponse.java deleted file mode 100644 index 26032a0..0000000 --- a/src/main/java/org/clevercastle/authforge/totp/RequestTotpResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -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/test/java/org/clevercastle/authforge/oauth2/oidc/OidcExchangeServiceTest.java b/src/test/java/org/clevercastle/authforge/oauth2/oidc/OidcExchangeServiceTest.java deleted file mode 100644 index 733ba44..0000000 --- a/src/test/java/org/clevercastle/authforge/oauth2/oidc/OidcExchangeServiceTest.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.clevercastle.authforge.oauth2.oidc; - -public class OidcExchangeServiceTest { -}