Skip to content

Commit fe28148

Browse files
committed
adding SSO Support
1 parent eeb6ba0 commit fe28148

25 files changed

+521
-36
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
/frontend/src/.DS_Store
88
/frontend/src/assets/.DS_Store
99
/backend/MixewayFlowAPI.iml
10-
/frontend/src/environments
10+
frontend/src/environments

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.9.1] - 2024-08-263
6+
### Changed
7+
- SSO integration introduced
8+
- Adjusted scripts to support SSO
9+
- Increased efficiency of running scans in parallel
10+
511
## [0.9.0] - 2024-08-13
612
### Changed
713
- Release of initial version - beta

backend/README.md

+6
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@
1111

1212
## First login
1313
`admin:admin` - then forced change
14+
15+
16+
### debug postgresql
17+
```shell
18+
docker run --name flow_db -e POSTGRES_PASSWORD=flow_pass -e POSTGRES_USER=flow_user -e POSTGRES_DB=flow -p 5433:5432 -v pgdata:/var/lib/postgresql/data -d postgres
19+
```

backend/entrypoint.sh

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
#!/bin/bash
22

3+
# Ensure required environment variables are set when SSO is enabled
4+
if [ "$(echo $SSO | tr '[:upper:]' '[:lower:]')" = "true" ]; then
5+
: "${SSO_CLIENT_ID:?SSO_CLIENT_ID is required when SSO is true}"
6+
: "${SSO_CLIENT_SECRET:?SSO_CLIENT_SECRET is required when SSO is true}"
7+
: "${SSO_REDIRECT_URI:?SSO_REDIRECT_URI is required when SSO is true}"
8+
: "${SSO_AUTHORIZATION_URI:?SSO_AUTHORIZATION_URI is required when SSO is true}"
9+
: "${SSO_TOKEN_URI:?SSO_TOKEN_URI is required when SSO is true}"
10+
: "${SSO_USER_INFO_URI:?SSO_USER_INFO_URI is required when SSO is true}"
11+
: "${SSO_JWK_SET_URI:?SSO_JWK_SET_URI is required when SSO is true}"
12+
13+
# Set the active profile to prodsso
14+
SPRING_PROFILE="prodsso"
15+
else
16+
# Set the default profile
17+
SPRING_PROFILE="prod"
18+
fi
19+
320
# Start Dependency-Track in the background with 4GB of memory and log output to a file
421
LOG_FILE="/var/log/dtrack.log"
522
echo "Starting Dependency-Track..."
@@ -72,11 +89,11 @@ if [ "$(echo $SSL | tr '[:upper:]' '[:lower:]')" = "true" ]; then
7289
git config --global https.proxy http://$PROXY_HOST:$PROXY_PORT
7390
fi
7491

75-
echo "Proceeding to run the application with SSL..."
92+
echo "Proceeding to run the application with SSL and profile $SPRING_PROFILE..."
7693
if [ -n "$PROXY_HOST" ] && [ -n "$PROXY_PORT" ]; then
77-
java -Dspring.profiles.active=prod -Dserver.ssl.key-store=/etc/pki/certificate.p12 -Dserver.ssl.key-store-password=$P12PASS -Dserver.ssl.key-alias=flow -Dproxy.host=$PROXY_HOST -Dproxy.port=$PROXY_PORT -jar /app/flowapi.jar
94+
java -Dspring.profiles.active=$SPRING_PROFILE -Dserver.ssl.key-store=/etc/pki/certificate.p12 -Dserver.ssl.key-store-password=$P12PASS -Dserver.ssl.key-alias=flow -Dproxy.host=$PROXY_HOST -Dproxy.port=$PROXY_PORT -jar /app/flowapi.jar
7895
else
79-
java -Dspring.profiles.active=prod -Dserver.ssl.key-store=/etc/pki/certificate.p12 -Dserver.ssl.key-store-password=$P12PASS -Dserver.ssl.key-alias=flow -jar /app/flowapi.jar
96+
java -Dspring.profiles.active=$SPRING_PROFILE -Dserver.ssl.key-store=/etc/pki/certificate.p12 -Dserver.ssl.key-store-password=$P12PASS -Dserver.ssl.key-alias=flow -jar /app/flowapi.jar
8097
fi
8198
else
8299
echo "SSL is not enabled. Running the application without SSL..."

backend/pom.xml

+10
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,16 @@
131131
<artifactId>junit</artifactId>
132132
<scope>test</scope>
133133
</dependency>
134+
135+
<dependency>
136+
<groupId>org.springframework.boot</groupId>
137+
<artifactId>spring-boot-starter-oauth2-client</artifactId>
138+
</dependency>
139+
<dependency>
140+
<groupId>org.keycloak</groupId>
141+
<artifactId>keycloak-spring-boot-starter</artifactId>
142+
<version>24.0.3</version>
143+
</dependency>
134144
</dependencies>
135145

136146
<build>

backend/src/main/java/io/mixeway/mixewayflowapi/api/auth/controller/AuthController.java

+23-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import jakarta.validation.Valid;
1616
import lombok.RequiredArgsConstructor;
1717
import lombok.extern.log4j.Log4j2;
18+
import org.springframework.context.annotation.Profile;
19+
import org.springframework.core.env.Environment;
1820
import org.springframework.http.HttpStatus;
1921
import org.springframework.http.MediaType;
2022
import org.springframework.http.ResponseEntity;
@@ -24,12 +26,14 @@
2426
import org.springframework.security.core.Authentication;
2527
import org.springframework.security.core.context.SecurityContextHolder;
2628
import org.springframework.security.core.userdetails.UsernameNotFoundException;
29+
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
2730
import org.springframework.validation.annotation.Validated;
2831
import org.springframework.web.bind.annotation.GetMapping;
2932
import org.springframework.web.bind.annotation.PostMapping;
3033
import org.springframework.web.bind.annotation.RequestBody;
3134
import org.springframework.web.bind.annotation.RestController;
3235

36+
import java.io.IOException;
3337
import java.security.Principal;
3438

3539
@RestController
@@ -41,7 +45,7 @@ public class AuthController {
4145
private final JwtService jwtService;
4246
private final AuthService authService;
4347
private final FindUserService findUserService;
44-
48+
private final Environment environment;
4549

4650
@PostMapping("/api/v1/login")
4751
public ResponseEntity<?> login(@Valid @RequestBody AuthRequestDTO authRequestDTO, HttpServletRequest request, HttpServletResponse response) {
@@ -95,13 +99,29 @@ public ResponseEntity<StatusDTO> changePassword(@Valid @RequestBody PassRequestD
9599

96100
@PreAuthorize("hasAuthority('USER')")
97101
@GetMapping("/api/v1/hc")
98-
public ResponseEntity<String> hc() {
102+
public ResponseEntity<StatusDTO> hc(Principal principal) {
103+
UserInfo userInfo = findUserService.findUser(principal.getName());
99104
try {
100-
return new ResponseEntity<>("", HttpStatus.OK);
105+
return new ResponseEntity<>(new StatusDTO(userInfo.getHighestRole()), HttpStatus.OK);
106+
} catch (Exception e){
107+
throw new RuntimeException(e);
108+
}
109+
}
110+
111+
@GetMapping("/api/v1/status")
112+
public ResponseEntity<StatusDTO> status() {
113+
String[] activeProfiles = environment.getActiveProfiles();
114+
String profile = "";
115+
if (activeProfiles.length > 0) {
116+
profile = activeProfiles[0];
117+
}
118+
try {
119+
return new ResponseEntity<>(new StatusDTO(profile), HttpStatus.OK);
101120
} catch (Exception e){
102121
throw new RuntimeException(e);
103122
}
104123
}
124+
105125
@PreAuthorize("hasAuthority('ADMIN')")
106126
@GetMapping("/api/v1/hc/admin")
107127
public ResponseEntity<String> hcAdmin() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.mixeway.mixewayflowapi.api.auth.service;
2+
3+
import lombok.extern.log4j.Log4j2;
4+
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
5+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
6+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
7+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
8+
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
9+
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
10+
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
11+
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
12+
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
13+
import org.springframework.security.oauth2.core.user.OAuth2User;
14+
import org.springframework.stereotype.Service;
15+
16+
import java.util.Collections;
17+
import java.util.Map;
18+
19+
@Service
20+
@Log4j2
21+
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
22+
23+
@Override
24+
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
25+
log.error("XAXAXAXAXAXAXAXAXAXA");
26+
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
27+
OAuth2User oAuth2User = delegate.loadUser(userRequest);
28+
29+
// Extracting ID Token and User Info attributes
30+
Map<String, Object> attributes = oAuth2User.getAttributes();
31+
OidcIdToken idToken = new OidcIdToken(
32+
userRequest.getAccessToken().getTokenValue(),
33+
userRequest.getAccessToken().getIssuedAt(),
34+
userRequest.getAccessToken().getExpiresAt(),
35+
attributes
36+
);
37+
OidcUserInfo userInfo = new OidcUserInfo(attributes);
38+
39+
// Creating OidcUser using OidcUserAuthority
40+
OidcUserAuthority authority = new OidcUserAuthority(idToken, userInfo);
41+
return new DefaultOidcUser(Collections.singleton(authority), idToken, userInfo);
42+
}
43+
}

backend/src/main/java/io/mixeway/mixewayflowapi/api/team/controller/TeamController.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public ResponseEntity<StatusDTO> createTeam(@Valid @RequestBody CreateTeamReques
4343
}
4444
}
4545

46-
@PreAuthorize("hasAuthority('TEAM_MANAGER')")
46+
@PreAuthorize("hasAuthority('USER')")
4747
@GetMapping(value= "/api/v1/team")
4848
public ResponseEntity<List<TeamDto>> getTeams(Principal principal){
4949
try {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.mixeway.mixewayflowapi.auth;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import jakarta.servlet.http.HttpServletResponse;
5+
import org.springframework.security.core.AuthenticationException;
6+
import org.springframework.security.web.AuthenticationEntryPoint;
7+
import org.springframework.stereotype.Component;
8+
9+
import java.io.IOException;
10+
11+
@Component
12+
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
13+
14+
@Override
15+
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
16+
if (request.getServletPath().startsWith("/api/")) {
17+
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
18+
} else {
19+
response.sendRedirect("/oauth2/authorization/sso");
20+
}
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package io.mixeway.mixewayflowapi.auth;
2+
3+
import io.mixeway.mixewayflowapi.api.user.dto.CreateUserRequestDto;
4+
import io.mixeway.mixewayflowapi.auth.jwt.JwtService;
5+
import io.mixeway.mixewayflowapi.db.entity.UserInfo;
6+
import io.mixeway.mixewayflowapi.domain.user.CreateUserService;
7+
import io.mixeway.mixewayflowapi.domain.user.FindUserService;
8+
import io.mixeway.mixewayflowapi.utils.Role;
9+
import jakarta.servlet.ServletException;
10+
import jakarta.servlet.http.Cookie;
11+
import jakarta.servlet.http.HttpServletRequest;
12+
import jakarta.servlet.http.HttpServletResponse;
13+
import lombok.RequiredArgsConstructor;
14+
import lombok.extern.log4j.Log4j2;
15+
import org.springframework.beans.factory.annotation.Value;
16+
import org.springframework.security.core.Authentication;
17+
import org.springframework.security.core.context.SecurityContextHolder;
18+
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
19+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
20+
import org.springframework.stereotype.Component;
21+
22+
import java.io.IOException;
23+
import java.util.ArrayList;
24+
25+
@Component
26+
@RequiredArgsConstructor
27+
@Log4j2
28+
public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {
29+
30+
private final JwtService jwtService;
31+
private final CreateUserService createUserService;
32+
private final FindUserService findUserService;
33+
@Value("${frontend.url}")
34+
String frontendUrl;
35+
36+
@Override
37+
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
38+
OidcUser oidcUser = (OidcUser) authentication.getPrincipal();
39+
String username = oidcUser.getPreferredUsername(); // Adjust based on your Keycloak config
40+
41+
UserInfo userInfo = findUserService.findUser(username);
42+
if (userInfo == null){
43+
userInfo = createUserService.createUser(CreateUserRequestDto.of(username, Role.USER, "xxxxxxxxxxxx", new ArrayList<>()));
44+
}
45+
String jwtToken = jwtService.GenerateToken(userInfo.getUsername(), userInfo.getHighestRole()); // Replace "USER_ROLE" with actual role logic
46+
SecurityContextHolder.getContext().setAuthentication(authentication);
47+
// Set the JWT token in an HTTP-only and secure cookie
48+
Cookie cookie = new Cookie("flow-token", jwtToken);
49+
cookie.setHttpOnly(true);
50+
cookie.setSecure(request.isSecure());
51+
cookie.setPath("/");
52+
cookie.setMaxAge(7 * 24 * 60 * 60);
53+
54+
response.addCookie(cookie);
55+
56+
if (frontendUrl == null) {
57+
throw new IllegalStateException("FRONTEND_URL environment variable must be set when SSO is enabled");
58+
}
59+
response.sendRedirect(frontendUrl);
60+
}
61+
}

backend/src/main/java/io/mixeway/mixewayflowapi/auth/jwt/JwtAuthFilter.java

+13
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
import lombok.extern.log4j.Log4j2;
1313
import org.springframework.beans.factory.annotation.Autowired;
1414
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
15+
import org.springframework.security.core.Authentication;
1516
import org.springframework.security.core.context.SecurityContextHolder;
1617
import org.springframework.security.core.userdetails.UserDetails;
1718
import org.springframework.security.core.userdetails.UsernameNotFoundException;
19+
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
1820
import org.springframework.security.oauth2.jwt.JwtValidationException;
1921
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
2022
import org.springframework.stereotype.Component;
@@ -32,6 +34,8 @@ public class JwtAuthFilter extends OncePerRequestFilter {
3234

3335
@Override
3436
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
37+
log.debug("JwtAuthFilter: Processing request " + request.getRequestURI());
38+
3539
String token = null;
3640
String username = null;
3741

@@ -79,4 +83,13 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
7983

8084
filterChain.doFilter(request, response);
8185
}
86+
87+
@Override
88+
protected boolean shouldNotFilter(HttpServletRequest request) {
89+
String path = request.getRequestURI();
90+
return path.startsWith("/api/v1/sso") ||
91+
path.startsWith("/oauth2/") ||
92+
path.startsWith("/api/v1/webhook/") ||
93+
path.startsWith("/api/v1/status");
94+
}
8295
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.mixeway.mixewayflowapi.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
6+
7+
@Configuration
8+
public class BCryptEncoderConfig {
9+
@Bean
10+
public BCryptPasswordEncoder passwordEncoder() {
11+
return new BCryptPasswordEncoder();
12+
}
13+
}

0 commit comments

Comments
 (0)