Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENG-0000: Account lock support #238

Merged
merged 5 commits into from
Feb 27, 2024
Merged
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
12 changes: 9 additions & 3 deletions .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
@@ -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
@@ -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


@@ -246,4 +246,10 @@ jobs:
uses: orgoro/[email protected]
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
@@ -4,8 +4,8 @@
replicaCount: 1
imagePullPolicy: Always

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

web:
image: crapi/crapi-web
2 changes: 1 addition & 1 deletion deploy/helm/values.yaml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions services/identity/.env
Original file line number Diff line number Diff line change
@@ -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 [email protected]
@@ -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
@@ -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
@@ -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;
@@ -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
@@ -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;
@@ -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);
@@ -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);
@@ -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;
}
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
@@ -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.*;
@@ -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;

@@ -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;

@@ -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 {
Original file line number Diff line number Diff line change
@@ -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() {
@@ -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);
}
@@ -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) -> {
@@ -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
@@ -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.";
Loading