I have a Spring Boot Backend with a React Frontend (with Material-UI) and I'm wanting to learn how to use Oauth2 with Spring Security to sign-up / login to my side project. In my Frontend I have the following code that has two buttons that are suppose to take me to the sign in pages for either google or github, but they do not.
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import Link from '@mui/material/Link';
import Box from '@mui/material/Box';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import { createTheme, ThemeProvider } from '@mui/material/styles';
function Copyright(props: any) {
return (
<Typography
variant='body2'
color='text.secondary'
align='center'
{...props}
>
{'Copyright © '}
<Link
color='inherit'
href='https://mui.com/'
>
Your Website
</Link>{' '}
{new Date().getFullYear()}
{'.'}
</Typography>
);
}
const theme = createTheme();
export default function LogIn() {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
console.log({
email: data.get('email'),
password: data.get('password'),
});
};
return (
<ThemeProvider theme={theme}>
<Container
component='main'
maxWidth='xs'
>
<Box
sx={{
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<LockOutlinedIcon />
</Avatar>
<Typography
component='h1'
variant='h5'
>
Sign in
</Typography>
<Link sx={{width: '100%'}} href={"/oauth2/authorization/github"} >
<Button
fullWidth
variant='contained'
sx={{ mt: 3, mb: 1 }}
>
GITHUB LOGIN
</Button>
</Link>
<Link sx={{width: '100%'}} href={"/oauth2/authorization/google"}>
<Button
fullWidth
variant='contained'
sx={{ mt: 1, mb: 1 }}
>
GOOGLE LOGIN
</Button>
</Link>
</Box>
</Container>
</ThemeProvider>
);
}
I also have my security configuration file as:
package SideProject.WorkoutJournal.SideProjectWorkoutJournal.SecurityConfigurations;
import SideProject.WorkoutJournal.SideProjectWorkoutJournal.Entities.AuthProvider;
import SideProject.WorkoutJournal.SideProjectWorkoutJournal.Entities.UserAccount;
import SideProject.WorkoutJournal.SideProjectWorkoutJournal.Repos.UserAccountRepo;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import java.io.IOException;
import java.util.StringJoiner;
import java.util.Objects;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
private UserAccountRepo userRepo;
public SecurityConfiguration(
UserAccountRepo userRepo
) {
this.userRepo = userRepo;
}
// Security Filter Chain --> Ordering of the security filters are important...
@Bean
SecurityFilterChain unsecuredFilter(
HttpSecurity http
) throws Exception {
http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.authorizeHttpRequests().requestMatchers("/","/log-in").permitAll()
.requestMatchers("/workoutMain/**").authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
// .sessionRegistry(sessionRegistry())
.and().and()
.headers()
.xssProtection() // Prevent's XSS Cross-Site Scripting
.and()
.contentSecurityPolicy(contentSecurityPolicy()) // Ensures that the origin types come from self, Prevents different types of Data Injections.
.and().and()
.oauth2Login()
.loginPage("/log-in")
.successHandler(
new SavedRequestAwareAuthenticationSuccessHandler(){
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
// Reason why I have to cast it into a DefaultOAuth2User is because what is returned is an Object.
// The principle object is always returned after a successful authentication.
DefaultOAuth2User oAuth2User = (DefaultOAuth2User) authentication.getPrincipal();
UserAccount newUser = getUserInfoFromAuth(request.getRequestURI(), oAuth2User);
//Setting the Username and picture if it changed.
if (Objects.nonNull(newUser) && Objects.nonNull(newUser.getProviderId())){
UserAccount user = userRepo.findByProviderId(newUser.getProviderId());
user.setImageURL(newUser.getImageURL());
user.setUsername(newUser.getUsername());
userRepo.save(user);
}
super.onAuthenticationSuccess(request, response, authentication);
}
}
)
.and()
// .formLogin().disable()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/"))
.logoutSuccessUrl("/log-in")
.deleteCookies("JSESSIONID", "XSRF-TOKEN")
.invalidateHttpSession(true);
// .and()
// TODO: Need to figure out the Filter Later
// .addFilterBefore()
return http.build();
}
private UserAccount getUserInfoFromAuth(String requestURI, DefaultOAuth2User oAuth2User) {
if (requestURI.endsWith("google")) {
return getGoogleUserInfo(oAuth2User);
} else if (requestURI.endsWith("github")) {
return getGithubUserInfo(oAuth2User);
}
return null;
}
private UserAccount getGithubUserInfo(DefaultOAuth2User oAuth2User) {
String authId = oAuth2User.getAttributes().get("id").toString();
String userName = oAuth2User.getAttributes().get("login").toString();
String avatarUrl = oAuth2User.getAttributes().get("avatar_url").toString();
return new UserAccount( userName, avatarUrl, authId, AuthProvider.Github);
}
private UserAccount getGoogleUserInfo(DefaultOAuth2User oAuth2User) {
String authId = oAuth2User.getAttributes().get("sub").toString();
String userName = oAuth2User.getAttributes().get("given_name").toString();
String avatarUrl = oAuth2User.getAttributes().get("picture").toString();
return new UserAccount(userName, avatarUrl, authId, AuthProvider.Google);
}
private String contentSecurityPolicy() {
StringJoiner cspDirectives = new StringJoiner(";");
return cspDirectives.add("default-src 'none'")
// When you say self, you're saying that you want spring to only trust
.add("script-src 'self'")
.add("connect-src 'self'")
.add("img-src 'self'")
.add("frame-ancestors 'none'")
.add("form-action 'self'")
.add("font-src 'self'")
.add("manifest-src 'self'")
.add("style-src 'self'").toString();
}
}
From all the other tutorials I've read I also included the clientId, clientSecret, and redirect URI into the application.yml.
spring:
security:
oauth2:
client:
registration:
google:
redirect-uri: "http://localhost:8080/login/oauth2/code.google.com"
clientId: ${GoogleClientID}
clientSecret: ${GoogleSecretID}
scope:
- email
- profile
github:
redirect-uri: "http://localhost:8080/login/oauth2/code/github.com"
clientId: ${GithubClientId}
clientSecret: ${GithubSecretKey}
datasource:
url: 'jdbc:postgresql://localhost:5430/postgres'
username: ${PostgressUsername}
password: ${PostgresPassword}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: create-drop
show_sql: true
properties:
hibernate:
format_sql: true
server:
port: '8080'
servlet:
session:
cookie:
secure: true
http-only: true
tracking-modes: COOKIE
shutdown: graceful
info:
session:
timeout: 900
warningPeriod: 60
I had the github oauth2 working initially when I didn't have my frontend and I had a barebones security configuration. I'm at a loss for how I should approach this.