Skip to content

Commit

Permalink
adding SSO Support
Browse files Browse the repository at this point in the history
  • Loading branch information
siewer committed Aug 26, 2024
1 parent eeb6ba0 commit fe28148
Show file tree
Hide file tree
Showing 25 changed files with 521 additions and 36 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
/frontend/src/.DS_Store
/frontend/src/assets/.DS_Store
/backend/MixewayFlowAPI.iml
/frontend/src/environments
frontend/src/environments
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

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

## [0.9.1] - 2024-08-263
### Changed
- SSO integration introduced
- Adjusted scripts to support SSO
- Increased efficiency of running scans in parallel

## [0.9.0] - 2024-08-13
### Changed
- Release of initial version - beta
Expand Down
6 changes: 6 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@

## First login
`admin:admin` - then forced change


### debug postgresql
```shell
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
```
23 changes: 20 additions & 3 deletions backend/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
#!/bin/bash

# Ensure required environment variables are set when SSO is enabled
if [ "$(echo $SSO | tr '[:upper:]' '[:lower:]')" = "true" ]; then
: "${SSO_CLIENT_ID:?SSO_CLIENT_ID is required when SSO is true}"
: "${SSO_CLIENT_SECRET:?SSO_CLIENT_SECRET is required when SSO is true}"
: "${SSO_REDIRECT_URI:?SSO_REDIRECT_URI is required when SSO is true}"
: "${SSO_AUTHORIZATION_URI:?SSO_AUTHORIZATION_URI is required when SSO is true}"
: "${SSO_TOKEN_URI:?SSO_TOKEN_URI is required when SSO is true}"
: "${SSO_USER_INFO_URI:?SSO_USER_INFO_URI is required when SSO is true}"
: "${SSO_JWK_SET_URI:?SSO_JWK_SET_URI is required when SSO is true}"

# Set the active profile to prodsso
SPRING_PROFILE="prodsso"
else
# Set the default profile
SPRING_PROFILE="prod"
fi

# Start Dependency-Track in the background with 4GB of memory and log output to a file
LOG_FILE="/var/log/dtrack.log"
echo "Starting Dependency-Track..."
Expand Down Expand Up @@ -72,11 +89,11 @@ if [ "$(echo $SSL | tr '[:upper:]' '[:lower:]')" = "true" ]; then
git config --global https.proxy http://$PROXY_HOST:$PROXY_PORT
fi

echo "Proceeding to run the application with SSL..."
echo "Proceeding to run the application with SSL and profile $SPRING_PROFILE..."
if [ -n "$PROXY_HOST" ] && [ -n "$PROXY_PORT" ]; then
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
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
else
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
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
fi
else
echo "SSL is not enabled. Running the application without SSL..."
Expand Down
10 changes: 10 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>24.0.3</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
Expand All @@ -24,12 +26,14 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.security.Principal;

@RestController
Expand All @@ -41,7 +45,7 @@ public class AuthController {
private final JwtService jwtService;
private final AuthService authService;
private final FindUserService findUserService;

private final Environment environment;

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

@PreAuthorize("hasAuthority('USER')")
@GetMapping("/api/v1/hc")
public ResponseEntity<String> hc() {
public ResponseEntity<StatusDTO> hc(Principal principal) {
UserInfo userInfo = findUserService.findUser(principal.getName());
try {
return new ResponseEntity<>("", HttpStatus.OK);
return new ResponseEntity<>(new StatusDTO(userInfo.getHighestRole()), HttpStatus.OK);
} catch (Exception e){
throw new RuntimeException(e);
}
}

@GetMapping("/api/v1/status")
public ResponseEntity<StatusDTO> status() {
String[] activeProfiles = environment.getActiveProfiles();
String profile = "";
if (activeProfiles.length > 0) {
profile = activeProfiles[0];
}
try {
return new ResponseEntity<>(new StatusDTO(profile), HttpStatus.OK);
} catch (Exception e){
throw new RuntimeException(e);
}
}

@PreAuthorize("hasAuthority('ADMIN')")
@GetMapping("/api/v1/hc/admin")
public ResponseEntity<String> hcAdmin() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.mixeway.mixewayflowapi.api.auth.service;

import lombok.extern.log4j.Log4j2;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.Map;

@Service
@Log4j2
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.error("XAXAXAXAXAXAXAXAXAXA");
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);

// Extracting ID Token and User Info attributes
Map<String, Object> attributes = oAuth2User.getAttributes();
OidcIdToken idToken = new OidcIdToken(
userRequest.getAccessToken().getTokenValue(),
userRequest.getAccessToken().getIssuedAt(),
userRequest.getAccessToken().getExpiresAt(),
attributes
);
OidcUserInfo userInfo = new OidcUserInfo(attributes);

// Creating OidcUser using OidcUserAuthority
OidcUserAuthority authority = new OidcUserAuthority(idToken, userInfo);
return new DefaultOidcUser(Collections.singleton(authority), idToken, userInfo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public ResponseEntity<StatusDTO> createTeam(@Valid @RequestBody CreateTeamReques
}
}

@PreAuthorize("hasAuthority('TEAM_MANAGER')")
@PreAuthorize("hasAuthority('USER')")
@GetMapping(value= "/api/v1/team")
public ResponseEntity<List<TeamDto>> getTeams(Principal principal){
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.mixeway.mixewayflowapi.auth;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
if (request.getServletPath().startsWith("/api/")) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
} else {
response.sendRedirect("/oauth2/authorization/sso");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.mixeway.mixewayflowapi.auth;

import io.mixeway.mixewayflowapi.api.user.dto.CreateUserRequestDto;
import io.mixeway.mixewayflowapi.auth.jwt.JwtService;
import io.mixeway.mixewayflowapi.db.entity.UserInfo;
import io.mixeway.mixewayflowapi.domain.user.CreateUserService;
import io.mixeway.mixewayflowapi.domain.user.FindUserService;
import io.mixeway.mixewayflowapi.utils.Role;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.ArrayList;

@Component
@RequiredArgsConstructor
@Log4j2
public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {

private final JwtService jwtService;
private final CreateUserService createUserService;
private final FindUserService findUserService;
@Value("${frontend.url}")
String frontendUrl;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
OidcUser oidcUser = (OidcUser) authentication.getPrincipal();
String username = oidcUser.getPreferredUsername(); // Adjust based on your Keycloak config

UserInfo userInfo = findUserService.findUser(username);
if (userInfo == null){
userInfo = createUserService.createUser(CreateUserRequestDto.of(username, Role.USER, "xxxxxxxxxxxx", new ArrayList<>()));
}
String jwtToken = jwtService.GenerateToken(userInfo.getUsername(), userInfo.getHighestRole()); // Replace "USER_ROLE" with actual role logic
SecurityContextHolder.getContext().setAuthentication(authentication);
// Set the JWT token in an HTTP-only and secure cookie
Cookie cookie = new Cookie("flow-token", jwtToken);
cookie.setHttpOnly(true);
cookie.setSecure(request.isSecure());
cookie.setPath("/");
cookie.setMaxAge(7 * 24 * 60 * 60);

response.addCookie(cookie);

if (frontendUrl == null) {
throw new IllegalStateException("FRONTEND_URL environment variable must be set when SSO is enabled");
}
response.sendRedirect(frontendUrl);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.jwt.JwtValidationException;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
Expand All @@ -32,6 +34,8 @@ public class JwtAuthFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.debug("JwtAuthFilter: Processing request " + request.getRequestURI());

String token = null;
String username = null;

Expand Down Expand Up @@ -79,4 +83,13 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse

filterChain.doFilter(request, response);
}

@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
return path.startsWith("/api/v1/sso") ||
path.startsWith("/oauth2/") ||
path.startsWith("/api/v1/webhook/") ||
path.startsWith("/api/v1/status");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.mixeway.mixewayflowapi.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class BCryptEncoderConfig {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Loading

0 comments on commit fe28148

Please sign in to comment.