Skip to content

Commit

Permalink
ENG-0000: Account lock support (#238)
Browse files Browse the repository at this point in the history
* Implement MFA and account lock

* Upgrade node
  • Loading branch information
piyushroshan authored Feb 27, 2024
1 parent 0020afa commit 089a236
Show file tree
Hide file tree
Showing 89 changed files with 19,552 additions and 10,254 deletions.
12 changes: 9 additions & 3 deletions .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 14

- name: Install newman
run: npm install -g newman
Expand All @@ -161,7 +161,7 @@ jobs:
if: failure()
uses: jwalton/gh-docker-logs@v2

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


Expand Down Expand Up @@ -246,4 +246,10 @@ jobs:
uses: orgoro/coverage@v3.1
with:
coverageFile: services/workshop/coverage.xml
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}

- name: node prettier
run: |
cd services/web
npm install
npm run lint
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
replicaCount: 1
imagePullPolicy: Always

enableLog4j: true
enableShellInjection: true
enableLog4j: false
enableShellInjection: false

web:
image: crapi/crapi-web
Expand Down
2 changes: 1 addition & 1 deletion deploy/helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Declare variables to be passed into your templates.

jwtSecret: crapi
enableLog4j: false
enableLog4j: true
enableShellInjection: true
imagePullPolicy: Always
apiGatewayServiceUrl: https://api.mypremiumdealership.com
Expand Down
6 changes: 3 additions & 3 deletions services/identity/.env
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ export DB_PORT=5432
export DB_USER=admin
export LANG=C.UTF-8
export SMTP_PASS=xxxxxxxxxxxxxx
export MAILHOG_HOST=mailhog
export MAILHOG_HOST=127.0.0.1
export SMTP_PORT=587
export ENABLE_LOG4J=false
export DB_HOST=127.0.0.1
export JAVA_TOOL_OPTIONS=-Xmx128m
export JAVA_TOOL_OPTIONS=-Xmx2048m
export DB_NAME=crapi
export SERVER_PORT=8989
export SMTP_FROM=no-reply@example.com
Expand All @@ -25,6 +25,6 @@ export TLS_ENABLED=false
export TLS_KEYSTORE_TYPE=PKCS12
export TLS_KEYSTORE=classpath:certs/server.p12
export TLS_KEYSTORE_PASSWORD=passw0rd
export TLS_KEY_PASSWORD=passw0rd
export TLS_KEY_PASSWORD=passw0rd
export TLS_KEY_ALIAS=identity
export JWKS=$(openssl base64 -in ./jwks.json -A)
3 changes: 2 additions & 1 deletion services/identity/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ org.gradle.jvmargs= \
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
-XX\:MaxHeapSize\=1024m -Xmx1024m
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@

package com.crapi.config;

import com.crapi.model.CRAPIResponse;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
Expand All @@ -37,10 +39,12 @@ public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authenticationException)
throws IOException, ServletException {

throws IOException, ServletException, LockedException {
CRAPIResponse crapiResponse = new CRAPIResponse();
crapiResponse.setMessage("Invalid Token");
crapiResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().println("{ \"error\": \"Invalid Token\" }");
response.getWriter().println(crapiResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

package com.crapi.config;

import com.crapi.constant.UserMessage;
import com.crapi.enums.EStatus;
import com.crapi.service.Impl.UserDetailsServiceImpl;
import jakarta.servlet.FilterChain;
Expand All @@ -31,6 +32,11 @@
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

enum ApiType {
JWT,
APIKEY;
}

public class JwtAuthTokenFilter extends OncePerRequestFilter {

private static final Logger tokenLogger = LoggerFactory.getLogger(JwtAuthTokenFilter.class);
Expand All @@ -55,11 +61,21 @@ protected void doFilterInternal(
String username = getUserFromToken(request);
if (username != null && !username.equalsIgnoreCase(EStatus.INVALID.toString())) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
if (userDetails == null) {
tokenLogger.error("User not found");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, UserMessage.INVALID_CREDENTIALS);
}
if (userDetails.isAccountNonLocked()) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
tokenLogger.error(UserMessage.ACCOUNT_LOCKED_MESSAGE);
response.sendError(
HttpServletResponse.SC_UNAUTHORIZED, UserMessage.ACCOUNT_LOCKED_MESSAGE);
}
}
} catch (Exception e) {
tokenLogger.error("Can NOT set user authentication -> Message:%d", e);
Expand All @@ -70,27 +86,47 @@ protected void doFilterInternal(

/**
* @param request
* @return jwt token
* @return key/token
*/
public String getJwt(HttpServletRequest request) {
public String getToken(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");

// checking token is there or not
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.replace("Bearer ", "");
if (authHeader != null && authHeader.length() > 7) {
return authHeader.substring(7);
}
return null;
}

/**
* @param request
* @return api type from HttpServletRequest
*/
public ApiType getKeyType(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
ApiType apiType = ApiType.JWT;
if (authHeader != null && authHeader.startsWith("ApiKey ")) {
apiType = ApiType.APIKEY;
}
return apiType;
}

/**
* @param request
* @return return username from HttpServletRequest if request have token we are returning username
* from request token
*/
public String getUserFromToken(HttpServletRequest request) throws ParseException {
String jwt = getJwt(request);
if (jwt != null && tokenProvider.validateJwtToken(jwt)) {
String username = tokenProvider.getUserNameFromJwtToken(jwt);
ApiType apiType = getKeyType(request);
String token = getToken(request);
String username = null;
if (token != null) {
if (apiType == ApiType.APIKEY) {
username = tokenProvider.getUserNameFromApiToken(token);
} else {
tokenProvider.validateJwtToken(token);
username = tokenProvider.getUserNameFromJwtToken(token);
}
// checking username from token
if (username != null) return username;
}
Expand Down
19 changes: 19 additions & 0 deletions services/identity/src/main/java/com/crapi/config/JwtProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package com.crapi.config;

import com.crapi.entity.User;
import com.crapi.repository.UserRepository;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.nimbusds.jose.*;
Expand All @@ -39,6 +40,7 @@
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

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

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

@Autowired private UserRepository userRepository;

@Value("${app.jwtExpiration}")
private String jwtExpiration;

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

/**
* @param token
* @return username from JWT Token
*/
public String getUserNameFromApiToken(String token) throws ParseException {
// Parse without verifying token signature
if (token != null) {
User user = userRepository.findByApiKey(token);
if (user != null) {
return user.getEmail();
}
}
return null;
}

// Load RSA Public Key for JKU header if present
private RSAKey getKeyFromJkuHeader(JWSHeader header) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,32 @@
package com.crapi.config;

import com.crapi.service.Impl.UserDetailsServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@Slf4j
@ComponentScan(basePackages = {"com.crapi"})
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

@Autowired UserDetailsServiceImpl userDetailsService;

@Autowired JwtAuthEntryPoint unauthorizedHandler;
@Autowired JwtAuthEntryPoint jwtUnauthorizedHandler;

@Bean
public JwtAuthTokenFilter authenticationJwtTokenFilter() {
Expand All @@ -61,8 +62,7 @@ public AuthenticationManager authenticationManager() throws Exception {
DaoAuthenticationProvider authProvider = authenticationProvider();
return new AuthenticationManager() {
@Override
public org.springframework.security.core.Authentication authenticate(
org.springframework.security.core.Authentication authentication)
public Authentication authenticate(Authentication authentication)
throws org.springframework.security.core.AuthenticationException {
return authProvider.authenticate(authentication);
}
Expand All @@ -75,7 +75,7 @@ public PasswordEncoder passwordEncoder() {
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain securityFilterChainWeb(HttpSecurity http) throws Exception {
http.cors(Customizer.withDefaults())
.csrf(
(csrf) -> {
Expand All @@ -90,14 +90,15 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.permitAll()
.requestMatchers("/identity/api/v2/user/dashboard")
.permitAll()
.requestMatchers("/identity/management/**")
.hasRole("ADMIN")
.anyRequest()
.authenticated())
.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(handling -> handling.authenticationEntryPoint(unauthorizedHandler));
.exceptionHandling(handling -> handling.authenticationEntryPoint(jwtUnauthorizedHandler));
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(
authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@

public class UserMessage {

public static final String LOGIN_SUCCESSFULL_MESSAGE = "Login successful";
public static final String OTP_REQUIRED_MESSAGE =
"User is locked. OTP has been sent to your email. Please provide that to unlock the account.";
public static final String API_KEY_GENERATED_MESSAGE =
"Api Key generated successfully. Use it in authorization header with ApiKey prefix.";
public static final String API_KEY_GENERATION_FAILED =
"Api Key generation failed! Only permitted for admin users.";
public static final String ACCOUNT_LOCK_MESSAGE = "User account has been locked.";
public static final String ACCOUNT_LOCKED_MESSAGE =
"User account is locked. Retry login with MFA to unlock.";
public static final String ACCOUNT_LOCK_FAILURE =
"Failed to lock the account. Please try again..";
public static final String ACCOUNT_UNLOCKED_MESSAGE = "User account is unlocked.";
public static final String INVALID_CREDENTIALS = "Invalid Credentials";
public static final String SIGN_UP_SUCCESS_MESSAGE =
"User registered successfully! Please Login.";
Expand Down
Loading

0 comments on commit 089a236

Please sign in to comment.