Skip to content

Commit c5c8c39

Browse files
authored
Merge pull request #45 from Wei-HaiMing/oAuthSetup
O auth setup
2 parents c424b38 + 851090e commit c5c8c39

File tree

10 files changed

+373
-5
lines changed

10 files changed

+373
-5
lines changed

build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ dependencies {
3131
testRuntimeOnly 'com.h2database:h2'
3232
implementation 'com.mysql:mysql-connector-j'
3333
implementation 'io.github.cdimascio:dotenv-java:3.0.0'
34+
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
35+
implementation 'org.springframework.boot:spring-boot-starter-security'
36+
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
37+
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
38+
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'
3439
}
3540

3641
tasks.named('test') {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.grouptwelve.grouptwelveBE.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.web.cors.CorsConfiguration;
6+
import org.springframework.web.cors.CorsConfigurationSource;
7+
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
8+
9+
import java.util.Arrays;
10+
11+
@Configuration
12+
public class CorsConfig {
13+
14+
@Bean
15+
public CorsConfigurationSource corsConfigurationSource() {
16+
CorsConfiguration configuration = new CorsConfiguration();
17+
18+
// Allow requests from specific origins
19+
// For development: allow all origins
20+
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
21+
22+
// configuration.setAllowedOrigins(Arrays.asList(
23+
// "https://your-production-domain.com",
24+
// "http://localhost:3000",
25+
// "exp://localhost:8081"
26+
// ));
27+
28+
// Allow all HTTP methods
29+
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"));
30+
31+
// Allow all headers
32+
configuration.setAllowedHeaders(Arrays.asList("*"));
33+
34+
// Expose headers to the client
35+
configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type"));
36+
37+
// Allow credentials (cookies, authorization headers)
38+
configuration.setAllowCredentials(true);
39+
40+
// How long the browser should cache the preflight response (in seconds)
41+
configuration.setMaxAge(3600L);
42+
43+
// Apply this configuration to all endpoints
44+
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
45+
source.registerCorsConfiguration("/**", configuration);
46+
47+
return source;
48+
}
49+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.grouptwelve.grouptwelveBE.config;
2+
3+
import com.grouptwelve.grouptwelveBE.security.OAuth2LoginSuccessHandler;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
8+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
9+
import org.springframework.security.web.SecurityFilterChain;
10+
import org.springframework.web.cors.CorsConfigurationSource;
11+
12+
@Configuration
13+
@EnableWebSecurity
14+
public class SecurityConfig {
15+
16+
@Autowired
17+
private OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
18+
19+
@Autowired
20+
private CorsConfigurationSource corsConfigurationSource;
21+
22+
@Bean
23+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
24+
http
25+
.cors(cors -> cors.configurationSource(corsConfigurationSource)) // Enable CORS
26+
.csrf(csrf -> csrf.disable()) // Disable CSRF for API testing
27+
.authorizeHttpRequests(auth -> auth
28+
.requestMatchers("/", "/login**", "/error", "/webjars/**").permitAll()
29+
.requestMatchers("/api/**").permitAll() // Allow all API endpoints without auth (for now)
30+
.requestMatchers("/auth/**").permitAll() // Allow auth endpoints
31+
.anyRequest().authenticated()
32+
)
33+
.oauth2Login(oauth2 -> oauth2
34+
.successHandler(oAuth2LoginSuccessHandler)
35+
)
36+
.logout(logout -> logout
37+
.logoutSuccessUrl("/")
38+
.permitAll()
39+
);
40+
41+
return http.build();
42+
}
43+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.grouptwelve.grouptwelveBE.controller;
2+
3+
import java.util.List;
4+
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpSession;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.web.bind.annotation.DeleteMapping;
9+
import org.springframework.web.bind.annotation.GetMapping;
10+
import org.springframework.web.bind.annotation.PatchMapping;
11+
import org.springframework.web.bind.annotation.PutMapping;
12+
import org.springframework.web.bind.annotation.PostMapping;
13+
import org.springframework.web.bind.annotation.RequestBody;
14+
import org.springframework.web.bind.annotation.RequestMapping;
15+
import org.springframework.web.bind.annotation.RequestParam;
16+
import org.springframework.web.bind.annotation.RestController;
17+
import org.springframework.web.servlet.view.RedirectView;
18+
19+
import com.grouptwelve.grouptwelveBE.model.User;
20+
import com.grouptwelve.grouptwelveBE.repository.UserRepository;
21+
import com.grouptwelve.grouptwelveBE.model.FavoriteTeam;
22+
import com.grouptwelve.grouptwelveBE.repository.FavoriteTeamRepository;
23+
24+
@RestController
25+
@RequestMapping("/auth")
26+
public class AuthController {
27+
28+
/**
29+
* OAuth start endpoint that accepts IP address parameter and redirects to GitHub OAuth
30+
* Usage: /auth/start?redirect_ip=192.168.1.5
31+
*/
32+
@GetMapping("/start")
33+
public RedirectView authStart(
34+
@RequestParam(value = "redirect_ip", required = false) String redirect_ip,
35+
HttpServletRequest request) {
36+
37+
// Store the redirect IP in session so it can be retrieved after OAuth callback
38+
if (redirect_ip != null && !redirect_ip.isEmpty()) {
39+
HttpSession session = request.getSession();
40+
session.setAttribute("redirect_ip", redirect_ip);
41+
System.out.println("Stored redirect_ip in session: " + redirect_ip);
42+
}
43+
44+
// Redirect directly to GitHub OAuth
45+
return new RedirectView("/oauth2/authorization/github");
46+
}
47+
48+
// @GetMapping("/callback")
49+
// public String authCallback() {
50+
// return "OAuth2 callback endpoint hit.";
51+
// }
52+
}

src/main/java/com/grouptwelve/grouptwelveBE/model/User.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77
public class User {
88
@Id
99
@GeneratedValue(strategy = GenerationType.IDENTITY)
10+
@Column(unique = true, nullable = false)
1011
private Long userId;
1112

1213
private String name;
14+
15+
@Column(unique = true)
1316
private String email;
17+
1418
private String password;
1519

1620
public User(){
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.grouptwelve.grouptwelveBE.security;
2+
3+
import com.grouptwelve.grouptwelveBE.util.JwtUtil;
4+
import jakarta.servlet.ServletException;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import jakarta.servlet.http.HttpSession;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.security.core.Authentication;
10+
import org.springframework.security.oauth2.core.user.OAuth2User;
11+
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
12+
import org.springframework.stereotype.Component;
13+
import com.grouptwelve.grouptwelveBE.model.User;
14+
import com.grouptwelve.grouptwelveBE.repository.UserRepository;
15+
import java.util.Optional;
16+
17+
import java.io.IOException;
18+
19+
@Component
20+
public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
21+
22+
@Autowired
23+
private JwtUtil jwtUtil;
24+
25+
@Autowired
26+
private UserRepository userRepository;
27+
28+
@Override
29+
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
30+
Authentication authentication) throws IOException, ServletException {
31+
32+
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
33+
34+
// Extract GitHub user information
35+
Integer githubIdInt = oAuth2User.getAttribute("id");
36+
Long githubId = githubIdInt != null ? githubIdInt.longValue() : null;
37+
String email = oAuth2User.getAttribute("email");
38+
String name = oAuth2User.getAttribute("name");
39+
String login = oAuth2User.getAttribute("login"); // GitHub username
40+
41+
// Use login as name if name is null
42+
final String finalName = (name == null || name.isEmpty()) ? login : name;
43+
44+
// Save or update user in database
45+
Optional<User> existingUser = userRepository.findById(githubId);
46+
User user;
47+
if (existingUser.isPresent()) {
48+
// User exists, just use it (don't update to avoid version conflicts)
49+
user = existingUser.get();
50+
} else {
51+
// New user, create and save
52+
User newUser = new User();
53+
newUser.setUserId(githubId);
54+
newUser.setEmail(email);
55+
newUser.setName(finalName);
56+
user = userRepository.save(newUser);
57+
}
58+
59+
// Generate JWT token (convert githubId to String)
60+
String jwtToken = jwtUtil.createToken(String.valueOf(githubId), email, finalName);
61+
62+
// Get the redirect IP from session (set during OAuth initiation)
63+
HttpSession session = request.getSession(false);
64+
String redirectIp = null;
65+
if (session != null) {
66+
redirectIp = (String) session.getAttribute("redirect_ip");
67+
// Clear the attribute after use
68+
session.removeAttribute("redirect_ip");
69+
}
70+
71+
String frontendUrl;
72+
if (redirectIp != null && !redirectIp.isEmpty()) {
73+
// Development mode: use exp:// with provided IP
74+
frontendUrl = "exp://" + redirectIp + ":8081/--/(tabs)/logout?token=" + jwtToken;
75+
} else {
76+
// Production mode: use custom scheme
77+
frontendUrl = "myapp://auth/callback?token=" + jwtToken;
78+
}
79+
80+
System.out.println("Redirecting to: " + frontendUrl);
81+
response.sendRedirect(frontendUrl);
82+
}
83+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package com.grouptwelve.grouptwelveBE.util;
2+
3+
import io.jsonwebtoken.Claims;
4+
import io.jsonwebtoken.Jwts;
5+
import io.jsonwebtoken.security.Keys;
6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.stereotype.Component;
8+
9+
import javax.crypto.SecretKey;
10+
import java.nio.charset.StandardCharsets;
11+
import java.util.Date;
12+
import java.util.HashMap;
13+
import java.util.Map;
14+
15+
@Component
16+
public class JwtUtil {
17+
18+
@Value("${jwt.secret}")
19+
private String jwtSecret;
20+
21+
@Value("${jwt.expirationMs}")
22+
private Long tokenValidityMillis;
23+
24+
// Create signing key from secret string
25+
private SecretKey getSigningKey() {
26+
byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8);
27+
return Keys.hmacShaKeyFor(keyBytes);
28+
}
29+
30+
// Build JWT with user information
31+
public String createToken(String userId, String email, String name) {
32+
Map<String, Object> claims = new HashMap<>();
33+
claims.put("userId", userId);
34+
claims.put("email", email);
35+
claims.put("name", name);
36+
37+
Date now = new Date();
38+
Date expiryDate = new Date(now.getTime() + tokenValidityMillis);
39+
40+
return Jwts.builder()
41+
.claims(claims)
42+
.subject(userId)
43+
.issuedAt(now)
44+
.expiration(expiryDate)
45+
.signWith(getSigningKey())
46+
.compact();
47+
}
48+
49+
// Extract all claims from token
50+
private Claims extractAllClaims(String token) {
51+
return Jwts.parser()
52+
.verifyWith(getSigningKey())
53+
.build()
54+
.parseSignedClaims(token)
55+
.getPayload();
56+
}
57+
58+
// Get specific claim from token
59+
private <T> T extractClaim(String token, java.util.function.Function<Claims, T> claimsResolver) {
60+
final Claims claims = extractAllClaims(token);
61+
return claimsResolver.apply(claims);
62+
}
63+
64+
// Extract user ID
65+
public String extractUserId(String token) {
66+
return extractClaim(token, claims -> claims.get("userId", String.class));
67+
}
68+
69+
// Extract email
70+
public String extractEmail(String token) {
71+
return extractClaim(token, claims -> claims.get("email", String.class));
72+
}
73+
74+
// Extract name
75+
public String extractName(String token) {
76+
return extractClaim(token, claims -> claims.get("name", String.class));
77+
}
78+
79+
// Extract expiration date
80+
public Date extractExpiration(String token) {
81+
return extractClaim(token, Claims::getExpiration);
82+
}
83+
84+
// Check if token is expired
85+
private Boolean isTokenExpired(String token) {
86+
return extractExpiration(token).before(new Date());
87+
}
88+
89+
// Validate token
90+
public Boolean validateToken(String token) {
91+
try {
92+
return !isTokenExpired(token);
93+
} catch (Exception e) {
94+
return false;
95+
}
96+
}
97+
}

src/main/resources/application-prod.properties

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,14 @@ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
66
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
77
spring.jpa.hibernate.ddl-auto=update
88
spring.datasource.hikari.connection-timeout=10000
9-
spring.datasource.hikari.maximum-pool-size=2
9+
spring.datasource.hikari.maximum-pool-size=2
10+
11+
# GitHub OAuth2
12+
spring.security.oauth2.client.registration.github.client-id=${GITHUB_CLIENT_ID}
13+
spring.security.oauth2.client.registration.github.client-secret=${GITHUB_CLIENT_SECRET}
14+
spring.security.oauth2.client.registration.github.scope=read:user,user:email
15+
spring.security.oauth2.client.registration.github.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
16+
17+
#JWT Configuration
18+
jwt.secret=${JWT_SECRET}
19+
jwt.expirationMs=3600000

src/main/resources/application-test.properties

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,14 @@ spring.datasource.username=sa
99
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
1010
spring.jpa.hibernate.ddl-auto=create-drop
1111
spring.jpa.show-sql=true
12-
spring.h2.console.enabled=true
12+
spring.h2.console.enabled=true
13+
14+
# JWT configuration for tests
15+
jwt.secret=test-secret-key-for-testing-only-should-be-at-least-256-bits-long
16+
jwt.expirationMs=3600000
17+
18+
# GitHub OAuth2 (dummy values for tests)
19+
spring.security.oauth2.client.registration.github.client-id=test-client-id
20+
spring.security.oauth2.client.registration.github.client-secret=test-client-secret
21+
spring.security.oauth2.client.registration.github.scope=read:user,user:email
22+
spring.security.oauth2.client.registration.github.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}

0 commit comments

Comments
 (0)