Skip to content

Commit cfde842

Browse files
Google OAuth2 login now working
1 parent 4b0f63e commit cfde842

12 files changed

Lines changed: 180 additions & 68 deletions

File tree

pom.xml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -402,11 +402,6 @@
402402
<groupId>org.springframework.boot</groupId>
403403
<artifactId>spring-boot-starter-oauth2-client</artifactId>
404404
</dependency>
405-
<dependency>
406-
<groupId>org.springframework.security</groupId>
407-
<artifactId>spring-security-oauth2-client</artifactId>
408-
<version>6.5.7</version>
409-
</dependency>
410405
<dependency>
411406
<groupId>org.springframework.boot</groupId>
412407
<artifactId>spring-boot-starter-cache</artifactId>

src/main/java/org/wise/portal/presentation/web/controllers/student/StudentAPIController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public class StudentAPIController extends UserAPIController {
9999
@Autowired
100100
private Properties i18nProperties;
101101

102-
@Value("${google.clientId:}")
102+
@Value("${spring.security.oauth2.client.registration.google.client-id}")
103103
private String googleClientId;
104104

105105
@GetMapping("/runs")

src/main/java/org/wise/portal/presentation/web/controllers/teacher/GoogleClassroomAPIController.java

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ public class GoogleClassroomAPIController {
4444
@Autowired
4545
private UserDetailsService userDetailsService;
4646

47-
@Value("${google.clientId:}")
47+
@Value("${spring.security.oauth2.client.registration.google.client-id}")
4848
private String googleClientId;
4949

50-
@Value("${google.clientSecret:}")
50+
@Value("${spring.security.oauth2.client.registration.google.client-secret}")
5151
private String googleClientSecret;
5252

5353
@Value("${wise.name:}")
@@ -79,8 +79,9 @@ private String googleOAuthToken(@RequestParam String code, HttpServletRequest re
7979
credentials.setClientSecret(googleClientSecret);
8080
GoogleClientSecrets clientSecrets = new GoogleClientSecrets();
8181
clientSecrets.setInstalled(credentials);
82-
GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT, JSON_FACTORY,
83-
clientSecrets, SCOPES).setDataStoreFactory(new FileDataStoreFactory(new java.io.File(tokensDirectoryPath)))
82+
GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT,
83+
JSON_FACTORY, clientSecrets, SCOPES)
84+
.setDataStoreFactory(new FileDataStoreFactory(new java.io.File(tokensDirectoryPath)))
8485
.build();
8586
TokenResponse response = flow.newTokenRequest(code).setRedirectUri(getRedirectUri()).execute();
8687
flow.createAndStoreCredential(response, username);
@@ -94,19 +95,22 @@ private ImmutablePair<String, Credential> authorize(String username) throws Exce
9495
credentials.setClientSecret(googleClientSecret);
9596
GoogleClientSecrets clientSecrets = new GoogleClientSecrets();
9697
clientSecrets.setInstalled(credentials);
97-
GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT, JSON_FACTORY,
98-
clientSecrets, SCOPES).setDataStoreFactory(new FileDataStoreFactory(new java.io.File(tokensDirectoryPath)))
98+
GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT,
99+
JSON_FACTORY, clientSecrets, SCOPES)
100+
.setDataStoreFactory(new FileDataStoreFactory(new java.io.File(tokensDirectoryPath)))
99101
.build();
100102
Credential credential = flow.loadCredential(username);
101-
if (credential != null && (credential.getRefreshToken() != null || credential.getExpiresInSeconds() == null ||
102-
credential.getExpiresInSeconds() > 60)) {
103+
if (credential != null && (credential.getRefreshToken() != null
104+
|| credential.getExpiresInSeconds() == null || credential.getExpiresInSeconds() > 60)) {
103105
return new ImmutablePair<>(null, credential);
104106
}
105107

106-
AuthorizationCodeRequestUrl authorizationUrl = flow.newAuthorizationUrl().setRedirectUri(getRedirectUri());
108+
AuthorizationCodeRequestUrl authorizationUrl = flow.newAuthorizationUrl()
109+
.setRedirectUri(getRedirectUri());
107110
String state = getState();
108111
String authorizationUri = authorizationUrl.setState(state).build();
109-
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
112+
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
113+
.currentRequestAttributes()).getRequest();
110114
request.getSession().setAttribute("state", state);
111115
return new ImmutablePair<>(authorizationUri, null);
112116
}
@@ -121,7 +125,8 @@ private String getRedirectUri() {
121125

122126
private Classroom connectToClassroomAPI(Credential credential) throws Exception {
123127
final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
124-
return new Classroom.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential).setApplicationName(applicationName).build();
128+
return new Classroom.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential)
129+
.setApplicationName(applicationName).build();
125130
}
126131

127132
@GetMapping("/get-authorization-url")
@@ -138,27 +143,27 @@ protected List<Course> getClassroomCourses(@RequestParam String username) throws
138143
return null;
139144
}
140145
List<Course> activeCourses = new ArrayList<>();
141-
List<Course> courses = connectToClassroomAPI(credential).courses().list().execute().getCourses();
146+
List<Course> courses = connectToClassroomAPI(credential).courses().list().execute()
147+
.getCourses();
142148
if (courses == null) {
143149
return activeCourses;
144150
}
145-
MutableUserDetails userDetails = (MutableUserDetails) userDetailsService.loadUserByUsername(username);
146-
for (Course course: courses) {
147-
if (!course.getCourseState().equals("ARCHIVED") && course.getOwnerId().equals(userDetails.getGoogleUserId())) {
151+
MutableUserDetails userDetails = (MutableUserDetails) userDetailsService
152+
.loadUserByUsername(username);
153+
for (Course course : courses) {
154+
if (!course.getCourseState().equals("ARCHIVED")
155+
&& course.getOwnerId().equals(userDetails.getGoogleUserId())) {
148156
activeCourses.add(course);
149157
}
150158
}
151159
return activeCourses;
152160
}
153161

154162
@PostMapping("/create-assignment")
155-
protected String addToClassroom(HttpServletRequest request,
156-
@RequestParam String accessCode,
157-
@RequestParam String unitTitle,
158-
@RequestParam String username,
159-
@RequestParam String endTime,
160-
@RequestParam String description,
161-
@RequestParam("courseIds") String courseIdsString) throws Exception {
163+
protected String addToClassroom(HttpServletRequest request, @RequestParam String accessCode,
164+
@RequestParam String unitTitle, @RequestParam String username, @RequestParam String endTime,
165+
@RequestParam String description, @RequestParam("courseIds") String courseIdsString)
166+
throws Exception {
162167
JSONObject response = new JSONObject();
163168
ImmutablePair<String, Credential> pair = authorize(username);
164169
String authorizationUrl = pair.getLeft();
@@ -218,7 +223,7 @@ private JSONObject addAssignmentToCourse(Classroom classroom, CourseWork coursew
218223
try {
219224
classroom.courses().courseWork().create(courseId, coursework).execute();
220225
courseJSON.put("success", true);
221-
} catch(Exception e) {
226+
} catch (Exception e) {
222227
courseJSON.put("success", false);
223228
}
224229
return courseJSON;

src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ public class TeacherAPIController extends UserAPIController {
5959
@Autowired
6060
private UserTagsService userTagsService;
6161

62-
@Value("${google.clientId:}")
62+
@Value("${spring.security.oauth2.client.registration.google.client-id}")
6363
private String googleClientId;
6464

65-
@Value("${google.clientSecret:}")
65+
@Value("${spring.security.oauth2.client.registration.google.client-secret}")
6666
private String googleClientSecret;
6767

6868
@GetMapping("/runs")

src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ public class UserAPIController {
8585
@Autowired
8686
protected StudentService studentService;
8787

88-
@Value("${google.clientId:}")
88+
@Value("${spring.security.oauth2.client.registration.google.client-id:}")
8989
protected String googleClientId = "";
9090

91-
@Value("${google.clientSecret:}")
91+
@Value("${spring.security.oauth2.client.registration.google.client-secret:}")
9292
private String googleClientSecret = "";
9393

9494
protected static final String PROJECT_THUMB_PATH = "/assets/project_thumb.png";
@@ -200,8 +200,7 @@ HashMap<String, Object> checkAuthentication(@RequestParam String username,
200200

201201
@PostMapping("/password")
202202
ResponseEntity<Map<String, Object>> changePassword(Authentication auth,
203-
@RequestParam String oldPassword,
204-
@RequestParam String newPassword) {
203+
@RequestParam String oldPassword, @RequestParam String newPassword) {
205204
if (!passwordService.isValid(newPassword)) {
206205
Map<String, Object> map = passwordService.getErrors(newPassword);
207206
return ResponseEntityGenerator.createError(map);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.wise.portal.presentation.web.filters;
2+
3+
import jakarta.servlet.ServletException;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import jakarta.servlet.http.HttpServletResponse;
6+
import org.springframework.security.core.AuthenticationException;
7+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
8+
import org.springframework.stereotype.Component;
9+
10+
import java.io.IOException;
11+
12+
/**
13+
* Handles failed OAuth2/OpenID Connect authentication attempts.
14+
* Redirects to an error page or login page with error message.
15+
*/
16+
@Component
17+
public class OAuth2AuthenticationFailureHandler implements AuthenticationFailureHandler {
18+
19+
@Override
20+
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
21+
AuthenticationException exception) throws IOException, ServletException {
22+
23+
// Log the error for debugging
24+
System.err.println("OAuth2 authentication failed: " + exception.getMessage());
25+
exception.printStackTrace();
26+
27+
// Redirect to login page with error parameter
28+
response.sendRedirect("/login?error=oauth2&message="
29+
+ java.net.URLEncoder.encode(exception.getMessage(), "UTF-8"));
30+
}
31+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package org.wise.portal.presentation.web.filters;
2+
3+
import jakarta.servlet.ServletException;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import jakarta.servlet.http.HttpServletResponse;
6+
import jakarta.servlet.http.HttpSession;
7+
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
10+
import org.springframework.security.core.Authentication;
11+
import org.springframework.security.core.context.SecurityContext;
12+
import org.springframework.security.core.context.SecurityContextHolder;
13+
import org.springframework.security.core.userdetails.UserDetails;
14+
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
15+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
16+
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
17+
import org.springframework.stereotype.Component;
18+
import org.wise.portal.domain.user.User;
19+
import org.wise.portal.service.user.UserService;
20+
21+
import java.io.IOException;
22+
23+
/**
24+
* Handles successful OAuth2/OpenID Connect authentication.
25+
* Looks up the WISE user by their Google user ID and creates a proper authentication
26+
* with WISE UserDetails (Teacher or Student).
27+
*/
28+
@Component
29+
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
30+
31+
@Autowired
32+
private UserService userService;
33+
34+
@Override
35+
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
36+
Authentication authentication) throws IOException, ServletException {
37+
38+
if (authentication.getPrincipal() instanceof OidcUser) {
39+
OidcUser oidcUser = (OidcUser) authentication.getPrincipal();
40+
String googleUserId = oidcUser.getSubject();
41+
42+
try {
43+
User user = userService.retrieveUserByGoogleUserId(googleUserId);
44+
if (user != null) {
45+
saveUserToSession(request, user);
46+
if (user.isStudent()) {
47+
response.sendRedirect("/student");
48+
} else {
49+
response.sendRedirect("/teacher");
50+
}
51+
} else {
52+
invalidateSession(request);
53+
response.sendRedirect("/join?googleUserNotFound=true");
54+
}
55+
} catch (Exception e) {
56+
invalidateSession(request);
57+
response.sendRedirect("/join?googleUserNotFound=true");
58+
}
59+
} else {
60+
response.sendRedirect("/");
61+
}
62+
}
63+
64+
private void saveUserToSession(HttpServletRequest request, User user) {
65+
UserDetails userDetails = user.getUserDetails();
66+
UsernamePasswordAuthenticationToken wiseAuth = new UsernamePasswordAuthenticationToken(
67+
userDetails, null, userDetails.getAuthorities());
68+
SecurityContext context = SecurityContextHolder.createEmptyContext();
69+
context.setAuthentication(wiseAuth);
70+
SecurityContextHolder.setContext(context);
71+
HttpSession session = request.getSession(true);
72+
session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context);
73+
}
74+
75+
private void invalidateSession(HttpServletRequest request) {
76+
HttpSession session = request.getSession(false);
77+
if (session != null) {
78+
session.invalidate();
79+
}
80+
}
81+
}

src/main/java/org/wise/portal/service/authentication/impl/UserDetailsServiceImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,12 @@ public UserDetails loadUserByUsername(String username)
6060
return userDetails;
6161
}
6262

63+
@Transactional(readOnly = true)
6364
public UserDetails loadUserByGoogleUserId(String googleUserId) {
6465
return this.userDetailsDao.retrieveByGoogleUserId(googleUserId);
6566
}
6667

68+
@Transactional(readOnly = true)
6769
public UserDetails loadUserByMicrosoftUserId(String userId) {
6870
return this.userDetailsDao.retrieveByMicrosoftUserId(userId);
6971
}

src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
5959
import org.springframework.session.Session;
6060
import org.wise.portal.presentation.web.filters.MicrosoftAuthenticationFailureHandler;
61+
import org.wise.portal.presentation.web.filters.OAuth2AuthenticationFailureHandler;
62+
import org.wise.portal.presentation.web.filters.OAuth2AuthenticationSuccessHandler;
6163
import org.wise.portal.presentation.web.filters.WISEAuthenticationFailureHandler;
6264
import org.wise.portal.presentation.web.filters.WISEAuthenticationProcessingFilter;
6365
import org.wise.portal.presentation.web.filters.WISEAuthenticationSuccessHandler;
@@ -104,6 +106,8 @@ public SecurityFilterChain filterChain(HttpSecurity http,
104106
.hasAnyRole("TEACHER").requestMatchers(new AntPathRequestMatcher("/student/**"))
105107
.hasAnyRole("STUDENT").requestMatchers(new AntPathRequestMatcher("/studentStatus"))
106108
.hasAnyRole("TEACHER", "STUDENT")
109+
.requestMatchers(new AntPathRequestMatcher("/oauth2/**")).permitAll()
110+
.requestMatchers(new AntPathRequestMatcher("/login/oauth2/**")).permitAll()
107111
.requestMatchers(new AntPathRequestMatcher("/api/google-login")).permitAll()
108112
.requestMatchers(new AntPathRequestMatcher("/api/*/register")).permitAll()
109113
.requestMatchers(new AntPathRequestMatcher("/api/teacher/**")).hasAnyRole("TEACHER")
@@ -118,6 +122,9 @@ public SecurityFilterChain filterChain(HttpSecurity http,
118122
.requestMatchers(new AntPathRequestMatcher("/")).permitAll().anyRequest()
119123
.authenticated())
120124
.formLogin(form -> form.loginPage("/login").permitAll())
125+
.oauth2Login(oauth2 -> oauth2.loginPage("/login")
126+
.successHandler(oauth2AuthenticationSuccessHandler())
127+
.failureHandler(oauth2AuthenticationFailureHandler()))
121128
.logout(logout -> logout.addLogoutHandler(wiseLogoutHandler())
122129
.logoutRequestMatcher(new AntPathRequestMatcher("/api/logout"))
123130
.logoutSuccessHandler((request, response, authentication) -> response
@@ -143,6 +150,16 @@ public OpenSessionInViewFilter openSessionInViewFilter() {
143150
return new OpenSessionInViewFilter();
144151
}
145152

153+
@Bean
154+
public OAuth2AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler() {
155+
return new OAuth2AuthenticationSuccessHandler();
156+
}
157+
158+
@Bean
159+
public OAuth2AuthenticationFailureHandler oauth2AuthenticationFailureHandler() {
160+
return new OAuth2AuthenticationFailureHandler();
161+
}
162+
146163
@Bean
147164
public LoggerListener loggerListener() {
148165
return new LoggerListener();

src/main/resources/application-dockerdev-sample.properties

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -179,17 +179,11 @@ trustedAuthorAllowedProjectAssetContentTypes=text/html,application/javascript,ap
179179

180180
########## Optional Plugins ##########
181181

182-
### Google Open Id (log in with Google) ###
183-
184-
google.clientId=
185-
google.clientSecret=
186-
187-
google.accessTokenUri=https://www.googleapis.com/oauth2/v3/token
188-
google.userAuthorizationUri=https://accounts.google.com/o/oauth2/auth?prompt=select_account
189-
google.redirectUri=http://localhost:81/api/google-login
190-
google.issuer=accounts.google.com
191-
google.jwkUrl=https://www.googleapis.com/oauth2/v2/certs
192-
google.tokens.dir=
182+
# OAuth2 Client Configuration for Google
183+
spring.security.oauth2.client.registration.google.client-id=
184+
spring.security.oauth2.client.registration.google.client-secret=
185+
spring.security.oauth2.client.registration.google.scope=openid,email,profile
186+
spring.security.oauth2.client.registration.google.redirect-uri=
193187

194188
### Microsoft Open Id (log in with Microsoft) ###
195189

0 commit comments

Comments
 (0)