Skip to content

Commit 089a236

Browse files
authored
ENG-0000: Account lock support (#238)
* Implement MFA and account lock * Upgrade node
1 parent 0020afa commit 089a236

File tree

89 files changed

+19552
-10254
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+19552
-10254
lines changed

.github/workflows/pr-build.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ jobs:
149149
- name: Install Node
150150
uses: actions/setup-node@v3
151151
with:
152-
node-version: 20
152+
node-version: 14
153153

154154
- name: Install newman
155155
run: npm install -g newman
@@ -161,7 +161,7 @@ jobs:
161161
if: failure()
162162
uses: jwalton/gh-docker-logs@v2
163163

164-
- name: Run crAPI using built images
164+
- name: Cleanup docker
165165
run: docker-compose -f deploy/docker/docker-compose.yml down --volumes --remove-orphans
166166

167167

@@ -246,4 +246,10 @@ jobs:
246246
uses: orgoro/[email protected]
247247
with:
248248
coverageFile: services/workshop/coverage.xml
249-
token: ${{ secrets.GITHUB_TOKEN }}
249+
token: ${{ secrets.GITHUB_TOKEN }}
250+
251+
- name: node prettier
252+
run: |
253+
cd services/web
254+
npm install
255+
npm run lint
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
replicaCount: 1
55
imagePullPolicy: Always
66

7-
enableLog4j: true
8-
enableShellInjection: true
7+
enableLog4j: false
8+
enableShellInjection: false
99

1010
web:
1111
image: crapi/crapi-web

deploy/helm/values.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Declare variables to be passed into your templates.
44

55
jwtSecret: crapi
6-
enableLog4j: false
6+
enableLog4j: true
77
enableShellInjection: true
88
imagePullPolicy: Always
99
apiGatewayServiceUrl: https://api.mypremiumdealership.com

services/identity/.env

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ export DB_PORT=5432
88
export DB_USER=admin
99
export LANG=C.UTF-8
1010
export SMTP_PASS=xxxxxxxxxxxxxx
11-
export MAILHOG_HOST=mailhog
11+
export MAILHOG_HOST=127.0.0.1
1212
export SMTP_PORT=587
1313
export ENABLE_LOG4J=false
1414
export DB_HOST=127.0.0.1
15-
export JAVA_TOOL_OPTIONS=-Xmx128m
15+
export JAVA_TOOL_OPTIONS=-Xmx2048m
1616
export DB_NAME=crapi
1717
export SERVER_PORT=8989
1818
export SMTP_FROM=[email protected]
@@ -25,6 +25,6 @@ export TLS_ENABLED=false
2525
export TLS_KEYSTORE_TYPE=PKCS12
2626
export TLS_KEYSTORE=classpath:certs/server.p12
2727
export TLS_KEYSTORE_PASSWORD=passw0rd
28-
export TLS_KEY_PASSWORD=passw0rd
28+
export TLS_KEY_PASSWORD=passw0rd
2929
export TLS_KEY_ALIAS=identity
3030
export JWKS=$(openssl base64 -in ./jwks.json -A)

services/identity/gradle.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ org.gradle.jvmargs= \
33
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
44
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
55
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
6-
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
6+
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
7+
-XX\:MaxHeapSize\=1024m -Xmx1024m

services/identity/src/main/java/com/crapi/config/JwtAuthEntryPoint.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414

1515
package com.crapi.config;
1616

17+
import com.crapi.model.CRAPIResponse;
1718
import jakarta.servlet.ServletException;
1819
import jakarta.servlet.http.HttpServletRequest;
1920
import jakarta.servlet.http.HttpServletResponse;
2021
import java.io.IOException;
22+
import org.springframework.security.authentication.LockedException;
2123
import org.springframework.security.core.AuthenticationException;
2224
import org.springframework.security.web.AuthenticationEntryPoint;
2325
import org.springframework.stereotype.Component;
@@ -37,10 +39,12 @@ public void commence(
3739
HttpServletRequest request,
3840
HttpServletResponse response,
3941
AuthenticationException authenticationException)
40-
throws IOException, ServletException {
41-
42+
throws IOException, ServletException, LockedException {
43+
CRAPIResponse crapiResponse = new CRAPIResponse();
44+
crapiResponse.setMessage("Invalid Token");
45+
crapiResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
4246
response.setContentType("application/json");
4347
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
44-
response.getOutputStream().println("{ \"error\": \"Invalid Token\" }");
48+
response.getWriter().println(crapiResponse);
4549
}
4650
}

services/identity/src/main/java/com/crapi/config/JwtAuthTokenFilter.java

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.crapi.config;
1616

17+
import com.crapi.constant.UserMessage;
1718
import com.crapi.enums.EStatus;
1819
import com.crapi.service.Impl.UserDetailsServiceImpl;
1920
import jakarta.servlet.FilterChain;
@@ -31,6 +32,11 @@
3132
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
3233
import org.springframework.web.filter.OncePerRequestFilter;
3334

35+
enum ApiType {
36+
JWT,
37+
APIKEY;
38+
}
39+
3440
public class JwtAuthTokenFilter extends OncePerRequestFilter {
3541

3642
private static final Logger tokenLogger = LoggerFactory.getLogger(JwtAuthTokenFilter.class);
@@ -55,11 +61,21 @@ protected void doFilterInternal(
5561
String username = getUserFromToken(request);
5662
if (username != null && !username.equalsIgnoreCase(EStatus.INVALID.toString())) {
5763
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
58-
UsernamePasswordAuthenticationToken authentication =
59-
new UsernamePasswordAuthenticationToken(
60-
userDetails, null, userDetails.getAuthorities());
61-
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
62-
SecurityContextHolder.getContext().setAuthentication(authentication);
64+
if (userDetails == null) {
65+
tokenLogger.error("User not found");
66+
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, UserMessage.INVALID_CREDENTIALS);
67+
}
68+
if (userDetails.isAccountNonLocked()) {
69+
UsernamePasswordAuthenticationToken authentication =
70+
new UsernamePasswordAuthenticationToken(
71+
userDetails, null, userDetails.getAuthorities());
72+
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
73+
SecurityContextHolder.getContext().setAuthentication(authentication);
74+
} else {
75+
tokenLogger.error(UserMessage.ACCOUNT_LOCKED_MESSAGE);
76+
response.sendError(
77+
HttpServletResponse.SC_UNAUTHORIZED, UserMessage.ACCOUNT_LOCKED_MESSAGE);
78+
}
6379
}
6480
} catch (Exception e) {
6581
tokenLogger.error("Can NOT set user authentication -> Message:%d", e);
@@ -70,27 +86,47 @@ protected void doFilterInternal(
7086

7187
/**
7288
* @param request
73-
* @return jwt token
89+
* @return key/token
7490
*/
75-
public String getJwt(HttpServletRequest request) {
91+
public String getToken(HttpServletRequest request) {
7692
String authHeader = request.getHeader("Authorization");
7793

7894
// checking token is there or not
79-
if (authHeader != null && authHeader.startsWith("Bearer ")) {
80-
return authHeader.replace("Bearer ", "");
95+
if (authHeader != null && authHeader.length() > 7) {
96+
return authHeader.substring(7);
8197
}
8298
return null;
8399
}
84100

101+
/**
102+
* @param request
103+
* @return api type from HttpServletRequest
104+
*/
105+
public ApiType getKeyType(HttpServletRequest request) {
106+
String authHeader = request.getHeader("Authorization");
107+
ApiType apiType = ApiType.JWT;
108+
if (authHeader != null && authHeader.startsWith("ApiKey ")) {
109+
apiType = ApiType.APIKEY;
110+
}
111+
return apiType;
112+
}
113+
85114
/**
86115
* @param request
87116
* @return return username from HttpServletRequest if request have token we are returning username
88117
* from request token
89118
*/
90119
public String getUserFromToken(HttpServletRequest request) throws ParseException {
91-
String jwt = getJwt(request);
92-
if (jwt != null && tokenProvider.validateJwtToken(jwt)) {
93-
String username = tokenProvider.getUserNameFromJwtToken(jwt);
120+
ApiType apiType = getKeyType(request);
121+
String token = getToken(request);
122+
String username = null;
123+
if (token != null) {
124+
if (apiType == ApiType.APIKEY) {
125+
username = tokenProvider.getUserNameFromApiToken(token);
126+
} else {
127+
tokenProvider.validateJwtToken(token);
128+
username = tokenProvider.getUserNameFromJwtToken(token);
129+
}
94130
// checking username from token
95131
if (username != null) return username;
96132
}

services/identity/src/main/java/com/crapi/config/JwtProvider.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.crapi.config;
1616

1717
import com.crapi.entity.User;
18+
import com.crapi.repository.UserRepository;
1819
import com.google.gson.Gson;
1920
import com.google.gson.GsonBuilder;
2021
import com.nimbusds.jose.*;
@@ -39,6 +40,7 @@
3940
import lombok.extern.slf4j.Slf4j;
4041
import org.slf4j.Logger;
4142
import org.slf4j.LoggerFactory;
43+
import org.springframework.beans.factory.annotation.Autowired;
4244
import org.springframework.beans.factory.annotation.Value;
4345
import org.springframework.stereotype.Component;
4446

@@ -48,6 +50,8 @@ public class JwtProvider {
4850

4951
private static final Logger logger = LoggerFactory.getLogger(JwtProvider.class);
5052

53+
@Autowired private UserRepository userRepository;
54+
5155
@Value("${app.jwtExpiration}")
5256
private String jwtExpiration;
5357

@@ -110,6 +114,21 @@ public String getUserNameFromJwtToken(String token) throws ParseException {
110114
return JWTParser.parse(token).getJWTClaimsSet().getSubject();
111115
}
112116

117+
/**
118+
* @param token
119+
* @return username from JWT Token
120+
*/
121+
public String getUserNameFromApiToken(String token) throws ParseException {
122+
// Parse without verifying token signature
123+
if (token != null) {
124+
User user = userRepository.findByApiKey(token);
125+
if (user != null) {
126+
return user.getEmail();
127+
}
128+
}
129+
return null;
130+
}
131+
113132
// Load RSA Public Key for JKU header if present
114133
private RSAKey getKeyFromJkuHeader(JWSHeader header) {
115134
try {

services/identity/src/main/java/com/crapi/config/WebSecurityConfig.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,32 @@
1515
package com.crapi.config;
1616

1717
import com.crapi.service.Impl.UserDetailsServiceImpl;
18+
import lombok.extern.slf4j.Slf4j;
1819
import org.springframework.beans.factory.annotation.Autowired;
1920
import org.springframework.context.annotation.Bean;
2021
import org.springframework.context.annotation.ComponentScan;
2122
import org.springframework.context.annotation.Configuration;
2223
import org.springframework.security.authentication.AuthenticationManager;
2324
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
2425
import org.springframework.security.config.Customizer;
25-
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
2626
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
2727
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
2828
import org.springframework.security.config.http.SessionCreationPolicy;
29+
import org.springframework.security.core.Authentication;
2930
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
3031
import org.springframework.security.crypto.password.PasswordEncoder;
3132
import org.springframework.security.web.SecurityFilterChain;
3233
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
3334

3435
@Configuration
3536
@EnableWebSecurity
37+
@Slf4j
3638
@ComponentScan(basePackages = {"com.crapi"})
37-
@EnableGlobalMethodSecurity(prePostEnabled = true)
3839
public class WebSecurityConfig {
3940

4041
@Autowired UserDetailsServiceImpl userDetailsService;
4142

42-
@Autowired JwtAuthEntryPoint unauthorizedHandler;
43+
@Autowired JwtAuthEntryPoint jwtUnauthorizedHandler;
4344

4445
@Bean
4546
public JwtAuthTokenFilter authenticationJwtTokenFilter() {
@@ -61,8 +62,7 @@ public AuthenticationManager authenticationManager() throws Exception {
6162
DaoAuthenticationProvider authProvider = authenticationProvider();
6263
return new AuthenticationManager() {
6364
@Override
64-
public org.springframework.security.core.Authentication authenticate(
65-
org.springframework.security.core.Authentication authentication)
65+
public Authentication authenticate(Authentication authentication)
6666
throws org.springframework.security.core.AuthenticationException {
6767
return authProvider.authenticate(authentication);
6868
}
@@ -75,7 +75,7 @@ public PasswordEncoder passwordEncoder() {
7575
}
7676

7777
@Bean
78-
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
78+
public SecurityFilterChain securityFilterChainWeb(HttpSecurity http) throws Exception {
7979
http.cors(Customizer.withDefaults())
8080
.csrf(
8181
(csrf) -> {
@@ -90,14 +90,15 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
9090
.permitAll()
9191
.requestMatchers("/identity/api/v2/user/dashboard")
9292
.permitAll()
93+
.requestMatchers("/identity/management/**")
94+
.hasRole("ADMIN")
9395
.anyRequest()
9496
.authenticated())
97+
.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
9598
.sessionManagement(
9699
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
97-
.exceptionHandling(handling -> handling.authenticationEntryPoint(unauthorizedHandler));
100+
.exceptionHandling(handling -> handling.authenticationEntryPoint(jwtUnauthorizedHandler));
98101
http.authenticationProvider(authenticationProvider());
99-
http.addFilterBefore(
100-
authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
101102
return http.build();
102103
}
103104
}

services/identity/src/main/java/com/crapi/constant/UserMessage.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@
1616

1717
public class UserMessage {
1818

19+
public static final String LOGIN_SUCCESSFULL_MESSAGE = "Login successful";
20+
public static final String OTP_REQUIRED_MESSAGE =
21+
"User is locked. OTP has been sent to your email. Please provide that to unlock the account.";
22+
public static final String API_KEY_GENERATED_MESSAGE =
23+
"Api Key generated successfully. Use it in authorization header with ApiKey prefix.";
24+
public static final String API_KEY_GENERATION_FAILED =
25+
"Api Key generation failed! Only permitted for admin users.";
26+
public static final String ACCOUNT_LOCK_MESSAGE = "User account has been locked.";
27+
public static final String ACCOUNT_LOCKED_MESSAGE =
28+
"User account is locked. Retry login with MFA to unlock.";
29+
public static final String ACCOUNT_LOCK_FAILURE =
30+
"Failed to lock the account. Please try again..";
31+
public static final String ACCOUNT_UNLOCKED_MESSAGE = "User account is unlocked.";
1932
public static final String INVALID_CREDENTIALS = "Invalid Credentials";
2033
public static final String SIGN_UP_SUCCESS_MESSAGE =
2134
"User registered successfully! Please Login.";

0 commit comments

Comments
 (0)