0

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.

dur
  • 15,689
  • 25
  • 79
  • 125
RogueGingerz
  • 97
  • 1
  • 9

1 Answers1

2

OAuth2 is a 3 tier thing:

  • authorization server: authenticates users and delivers tokens
  • resource server: validates tokens, implements access control, serves resources
  • client: initiates OAuth2 flows, fetches and stores tokens => it is client responsibility to initiate OAuth2 login (start authorization code flow by redirecting to authorization server) when it needs access to a secured resource from resource server (but it could also use other OAuth2 flows to fetch tokens: refresh token or client credentials)

Only the authorization server should have access to user credentials and I advise you don't write it yourself (use Keycloak or a cloud solution like Auth0 or Amazon Cognito that you can configure with a GUI and connect to the identity providers you like (Google Github and whatever you need)

Are you sure you want to configure your backend (most probably a REST API) as an OAuth2 (stateful) client and not an OAuth2 (stateless) resource server? Because REST APIs are usually configured as resource servers.

Requests to a client are secure with sessions, only requests from a client to a resource server are secured with access tokens. I remind such OAuth2 essential concepts as introduction to my tutorials which contain guidance to configuring clients and resource servers with Spring Boot "official" starters or using thin wrappers around it (further pushes auto-configuration from properties).

Regarding your React app, two options:

  • configure it as an OAuth2 public client using a client lib (search for OIDC or OpenID or OAuth2 for React and choose one), but it is not the trend
  • put a Backend For Frontend on your server (a middleware configured as OAuth2 client and replacing session cookies with OAuth2 access tokens before forwarding requests from React app to REST API). spring-cloud-gateway can be configured as BFF. Tutorial here

In both cases above, your REST API will be a stateless resource server queried by an OAuth2 client. In the first case, this client is your React app, in the second, it is the BFF on your server. In first case, requests from React app to backend are secured with OAuth2 access tokens, in the second with session cookies (but I already wrote that).

ch4mp
  • 6,622
  • 6
  • 29
  • 49
  • You seem like you might know the answer to my question here. If you can take a look, I'd appreciate it! https://stackoverflow.com/questions/75967998/how-can-users-grant-permission-for-my-website-to-manage-their-amazon-alexa-lists – Ryan Apr 14 '23 at 01:27