Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/mfa.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Mfa Flow

```mermaid
flowchart TD
A[User login] --> B{Authenticated?}
B -- No --> Z[Fail to login]
B -- Yes --> C[Show the mfa setup options]
C --> D[User: Setup mfa]
D --> E[Server generate the secret key and return the QR code string and one session-id, save this secret key in the system with key session-id]
E --> F[Client: Show the QR code to the user,]
F --> G[User: user use authenticator app to scan the QR code]
G --> H[User: user input the first verification code]
H --> I[Client: pass the session-id and the verification code to the server]
I --> J[Server: get the secret key based on the session-id and verify the code]
J -- Success to verify --> K[Server: Notify the user to input the verification code again]
J -- Fail to verify --> L[Server: Save the secret key to the database and return success]
K --> I
```
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.clevercastle.authforge.exception.CastleException;
import org.clevercastle.authforge.entity.User;
import org.clevercastle.authforge.entity.UserLoginItem;
import org.clevercastle.authforge.UserRegisterRequest;
import org.clevercastle.authforge.UserService;
import org.clevercastle.authforge.UserWithToken;
import org.clevercastle.authforge.dto.OneTimePasswordDto;
import org.clevercastle.authforge.exception.CastleException;
import org.clevercastle.authforge.model.User;
import org.clevercastle.authforge.model.UserLoginItem;
import org.clevercastle.authforge.oauth2.Oauth2ClientConfig;
import org.clevercastle.authforge.oauth2.github.GithubOauth2ExchangeService;
import org.clevercastle.authforge.oauth2.oidc.OidcExchangeService;
Expand Down Expand Up @@ -84,9 +85,10 @@ public User register(@RequestBody RegisterRequest request) throws CastleExceptio
return userService.register(userRegisterRequest);
}


@GetMapping("auth/verify")
public UserWithToken verify(@RequestParam String loginIdentifier, @RequestParam String verificationCode) throws CastleException {
userService.verify("email#" + loginIdentifier, verificationCode);
public UserWithToken verify(@RequestParam String email, @RequestParam String verificationCode) throws CastleException {
userService.verify("email#" + email, verificationCode);
return null;
}

Expand Down Expand Up @@ -140,4 +142,14 @@ public UserWithToken exchange(@RequestParam SsoType ssoType, @RequestParam Strin
}
return null;
}

@GetMapping("auth/one-time-password")
public OneTimePasswordDto requestOneTimePassword(@RequestParam String email) throws CastleException {
return userService.requestOneTimePassword("email#" + email);
}

@PostMapping("auth/one-time-password")
public UserWithToken verifyOneTimePassword(@RequestBody VerifyOneTimeRequest request) throws CastleException {
return userService.verifyOneTimePassword("email#" + request.getEmail(), request.getOneTimePassword());
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
package org.clevercastle.authforge.examples.springboot.springbootexample;

import com.auth0.jwt.algorithms.Algorithm;
import org.clevercastle.authforge.DummyCacheServiceImpl;
import org.clevercastle.authforge.Config;
import org.clevercastle.authforge.UserService;
import org.clevercastle.authforge.UserServiceImpl;
import org.clevercastle.authforge.repository.UserRepository;
import org.clevercastle.authforge.repository.dynamodb.DynamodbUser;
import org.clevercastle.authforge.repository.dynamodb.DynamodbUserRepositoryImpl;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaChallengeSessionRepository;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaOneTimePasswordRepository;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserHmacSecretRepository;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserLoginItemRepository;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserModelRepository;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserRefreshTokenMappingRepository;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserRepositoryImpl;
import org.clevercastle.authforge.token.TokenService;
import org.clevercastle.authforge.token.jwt.JwtTokenService;
import org.clevercastle.authforge.verification.DummyVerificationService;
import org.clevercastle.authforge.code.DummyCodeSender;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
Expand All @@ -37,8 +40,12 @@ public class Beans {
@Bean
public UserRepository userRepository(RdsJpaUserModelRepository userModelRepository,
RdsJpaUserLoginItemRepository userLoginItemRepository,
RdsJpaUserRefreshTokenMappingRepository userRefreshTokenMappingRepository) {
return new RdsJpaUserRepositoryImpl(userModelRepository, userLoginItemRepository, userRefreshTokenMappingRepository);
RdsJpaUserRefreshTokenMappingRepository userRefreshTokenMappingRepository,
RdsJpaOneTimePasswordRepository oneTimePasswordRepository,
RdsJpaChallengeSessionRepository challengeSessionRepository,
RdsJpaUserHmacSecretRepository userHmacSecretRepository) {
return new RdsJpaUserRepositoryImpl(userModelRepository, userLoginItemRepository,
userRefreshTokenMappingRepository, oneTimePasswordRepository, challengeSessionRepository, userHmacSecretRepository);
}


Expand Down Expand Up @@ -77,8 +84,8 @@ public TokenService tokenService() throws NoSuchAlgorithmException, InvalidKeySp
}

@Bean
public UserService userService(UserRepository dynamodbUserRepository, TokenService tokenService) {
return new UserServiceImpl(Config.builder().build(), dynamodbUserRepository, tokenService, new DummyVerificationService(dynamodbUserRepository));
public UserService userService(UserRepository userRepository, TokenService tokenService) {
return new UserServiceImpl(Config.builder().build(), userRepository, tokenService, new DummyCodeSender(), new DummyCacheServiceImpl());
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.clevercastle.authforge.examples.springboot.springbootexample;

import org.clevercastle.authforge.model.ChallengeSession;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaChallengeSessionRepository;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ChallengeSessionRepositoryAdapter extends RdsJpaChallengeSessionRepository, JpaRepository<ChallengeSession, String> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.clevercastle.authforge.examples.springboot.springbootexample;

import org.clevercastle.authforge.model.OneTimePassword;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaOneTimePasswordId;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaOneTimePasswordRepository;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OneTimePasswordRepositoryAdapter extends RdsJpaOneTimePasswordRepository, JpaRepository<OneTimePassword, RdsJpaOneTimePasswordId> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.clevercastle.authforge.examples.springboot.springbootexample;

public class SendOneTimeRequest {
private String email;

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.clevercastle.authforge.examples.springboot.springbootexample;

import org.clevercastle.authforge.entity.User;
import org.clevercastle.authforge.model.User;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.clevercastle.authforge.examples.springboot.springbootexample;

import org.clevercastle.authforge.UserService;
import org.clevercastle.authforge.exception.CastleException;
import org.clevercastle.authforge.model.User;
import org.clevercastle.authforge.totp.RequestTotpResponse;
import org.clevercastle.authforge.totp.SetupTotpRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

private final UserService userService;

public UserController(UserService userService) {
this.userService = userService;
}

@PostMapping("user/mfa/request")
public RequestTotpResponse requestTotp() throws CastleException {
User user = new User();
return userService.requestTotp(user);
}

@PostMapping("user/mfa/verify")
public void verifyTotp(@RequestBody SetupTotpRequest request) throws CastleException {
User user = new User();
user.setUserId("user-01");
userService.setupTotp(user, request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.clevercastle.authforge.examples.springboot.springbootexample;

import org.clevercastle.authforge.model.ChallengeSession;
import org.clevercastle.authforge.model.UserHmacSecret;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaChallengeSessionRepository;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserHmacSecretId;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserHmacSecretRepository;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserHmacSecretRepositoryAdapter extends RdsJpaUserHmacSecretRepository, JpaRepository<UserHmacSecret, RdsJpaUserHmacSecretId> {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.clevercastle.authforge.examples.springboot.springbootexample;

import org.clevercastle.authforge.entity.UserLoginItem;
import org.clevercastle.authforge.model.UserLoginItem;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserLoginItemRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.clevercastle.authforge.examples.springboot.springbootexample;

import org.clevercastle.authforge.entity.User;
import org.clevercastle.authforge.model.User;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserModelRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.clevercastle.authforge.examples.springboot.springbootexample;

import org.clevercastle.authforge.entity.UserRefreshTokenMapping;
import org.clevercastle.authforge.model.UserRefreshTokenMapping;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserRefreshTokenMappingRepository;
import org.clevercastle.authforge.repository.rdsjpa.RdsJpaUserRefreshTokenMappingId;
import org.springframework.data.jpa.repository.JpaRepository;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.clevercastle.authforge.examples.springboot.springbootexample;

public class VerifyOneTimeRequest {
private String email;
private String oneTimePassword;

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getOneTimePassword() {
return oneTimePassword;
}

public void setOneTimePassword(String oneTimePassword) {
this.oneTimePassword = oneTimePassword;
}
}
9 changes: 9 additions & 0 deletions src/main/java/org/clevercastle/authforge/CacheService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.clevercastle.authforge;

public interface CacheService {
void set(String key, String value, long ttl);

String get(String key);

boolean delete(String key);
}
26 changes: 17 additions & 9 deletions src/main/java/org/clevercastle/authforge/Config.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package org.clevercastle.authforge;

public class Config {
// in seconds
// in second
private int verificationCodeExpireTime;
// in seconds
// in second
private int tokenExpireTime;
// in second
private int oneTimePasswordExpireTime;

public int getVerificationCodeExpireTime() {
return verificationCodeExpireTime;
Expand All @@ -14,22 +16,22 @@ public int getTokenExpireTime() {
return tokenExpireTime;
}

public int getOneTimePasswordExpireTime() {
return oneTimePasswordExpireTime;
}

public static ConfigBuilder builder() {
return new ConfigBuilder();
}

public static final class ConfigBuilder {
private int verificationCodeExpireTime = 300;
private int tokenExpireTime = 28400;
private int verificationCodeExpireTime;
private int tokenExpireTime;
private int oneTimePasswordExpireTime;

private ConfigBuilder() {
}

public static ConfigBuilder aConfig() {
return new ConfigBuilder();
}

public ConfigBuilder verificationCodeExpireTime(int verificationCodeExpireTime) {
this.verificationCodeExpireTime = verificationCodeExpireTime;
return this;
Expand All @@ -40,10 +42,16 @@ public ConfigBuilder tokenExpireTime(int tokenExpireTime) {
return this;
}

public ConfigBuilder oneTimePasswordExpireTime(int oneTimePasswordExpireTime) {
this.oneTimePasswordExpireTime = oneTimePasswordExpireTime;
return this;
}

public Config build() {
Config config = new Config();
config.verificationCodeExpireTime = this.verificationCodeExpireTime;
config.tokenExpireTime = this.tokenExpireTime;
config.oneTimePasswordExpireTime = this.oneTimePasswordExpireTime;
config.verificationCodeExpireTime = this.verificationCodeExpireTime;
return config;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.clevercastle.authforge;

import org.apache.commons.lang3.StringUtils;

import java.util.HashMap;
import java.util.Map;

public class DummyCacheServiceImpl implements CacheService {
Map<String, String> map = new HashMap<>();

@Override
public void set(String key, String value, long ttl) {
map.put(key, value);
}

@Override
public String get(String key) {
return map.get(key);
}

@Override
public boolean delete(String key) {
String value = map.remove(key);
return StringUtils.isNotBlank(value);
}
}
21 changes: 19 additions & 2 deletions src/main/java/org/clevercastle/authforge/UserService.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package org.clevercastle.authforge;

import org.apache.commons.lang3.tuple.Pair;
import org.clevercastle.authforge.entity.User;
import org.clevercastle.authforge.entity.UserLoginItem;
import org.clevercastle.authforge.dto.OneTimePasswordDto;
import org.clevercastle.authforge.exception.CastleException;
import org.clevercastle.authforge.model.ChallengeSession;
import org.clevercastle.authforge.model.User;
import org.clevercastle.authforge.model.UserLoginItem;
import org.clevercastle.authforge.oauth2.Oauth2ClientConfig;
import org.clevercastle.authforge.totp.RequestTotpResponse;
import org.clevercastle.authforge.totp.SetupTotpRequest;

public interface UserService {
// used for username/password, email/password, mobile/password
Expand All @@ -24,4 +28,17 @@ public interface UserService {

// used for sso login
UserWithToken exchange(Oauth2ClientConfig clientConfig, String authorizationCode, String state, String redirectUrl) throws CastleException;

// one time password login
OneTimePasswordDto requestOneTimePassword(String loginIdentifier) throws CastleException;

UserWithToken verifyOneTimePassword(String loginIdentifier, String oneTimePassword) throws CastleException;

// mfa challenge
ChallengeSession createChallenge(String userId, ChallengeSession.Type type);

RequestTotpResponse requestTotp(User user) throws CastleException;

// setup mfa
void setupTotp(User user, SetupTotpRequest request) throws CastleException;
}
Loading
Loading