Monday, April 24, 2023

Spring Security 6 Custom Login, OAuth2 Login with Google and Basic Auth

 

Introduction:


Spring Security is a framework that provides authentication, authorization, and protection against common attacks. With first class support for securing both imperative and reactive applications, it is the de-facto standard for securing Spring-based applications.

Consuming Authentication objects:

Spring security provides Authentication
  • Authentication (authn): represent the users.
    • Principal: (name, email etc)
    • GrantedAuthorities: roles.
  • Authorization (authz): are the users  allowed to perform.


Demos: 

In this post I integrate Custom Login Form, OAuth2 Login with Google and Basic Auth.


Technology


  • Spring Boot 3.0.6
  • Java 17
  • OAuth2
  • Maven 
  • IntelliJ IDEA

Security Configuration:


SecurityConfig.java: In this class, I configure Custom Login, OAuth2 Login with Google and Basic Auth. sometimes we need apply some filter before, or some case we need test with user admin in this case I create the class AdminAuthenticateProvider without password.

package com.henry.springsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {


@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

return http
.authorizeRequests( authorizeConfig -> {
authorizeConfig.requestMatchers("/").permitAll();
authorizeConfig.requestMatchers("/login/**").permitAll();
authorizeConfig.requestMatchers("/error").permitAll();
authorizeConfig.requestMatchers("/favicon.ico").permitAll();
authorizeConfig.anyRequest().authenticated();
})
.formLogin( login -> {
login.loginPage("/login").permitAll();
login.defaultSuccessUrl("/private");
login.failureUrl("/login?error=true").permitAll();
}) //Normal Login
.logout(logout -> {
logout.logoutSuccessUrl("/login?logout=true").permitAll();
logout.invalidateHttpSession(true).permitAll();
logout.deleteCookies("JSESSIONID").permitAll();
})
.httpBasic(Customizer.withDefaults()) // support basic auth
.oauth2Login(oauth -> {
oauth.loginPage("/login").permitAll();
oauth.defaultSuccessUrl("/private");
oauth.failureUrl("/login?error=true").permitAll();
}) // OpenID Connect with google
.addFilterBefore(new CustFilter(), UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(new AdminAuthenticateProvider())
.csrf()
.disable()
.build();
}

@Bean
public UserDetailsService userDetailsService(){
return new InMemoryUserDetailsManager(
User.builder()
.username("henry")
.password("{noop}password")
.authorities("ROLE_USER")
.build()
);
}

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring() .requestMatchers("/resources/**", "/static/**", "/css/**");
}
}


AdminAuthenticateProvider.java

package com.henry.springsecurity.config;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;

public class AdminAuthenticateProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
var username = authentication.getName();
if("admin".equalsIgnoreCase(username)){
return UsernamePasswordAuthenticationToken.authenticated(
"admin",
null,
AuthorityUtils.createAuthorityList("ROLE_ADMIN")
);
}
return null;
}

@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}

CustFilter.java

package com.henry.springsecurity.config;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class CustFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
System.out.println("Hello filter");
filterChain.doFilter(request, response);
}
}




OAuth2 Login with Google Configuration:


  1. Create new project or choose existing project.
  2. Go to APIs & Services or Type oauth in search bar
  3. Choose  Credentials 
  4. Click on CREATE CREDENTIALS ->  OAuth client ID 
  5. Application Type -> Web application
  6. Name -> your app name
  7. Authorized redirect URIs: http://localhost:8080/login/oauth2/code/google
  8. CREATE





View 


Create a login page our own.


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Please Log In</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" >
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" >
<link rel="stylesheet" href="/css/login.css" type="text/css"/>
</head>
<body>
<section class="vh-100">
<div class="container-fluid h-custom">
<div class="row d-flex justify-content-center align-items-center h-100">
<div class="col-md-8 col-lg-6 col-xl-4 offset-xl-1">
<div
class="d-flex flex-row align-items-center justify-content-center justify-content-lg-start">
<p class="lead fw-normal mb-0 me-3">Login with OAuth 2.0</p>
</div>
<div class="d-flex flex-row align-items-center justify-content-center justify-content-lg-start">
<table class="table table-striped">
<tbody>
<tr>
<td><a href="/oauth2/authorization/google">Google</a></td>
</tr>
</tbody>
</table>
</div>
<form th:action="@{/login}" method="post">
<div class="divider d-flex align-items-center my-4">
<p class="text-center fw-bold mx-3 mb-0">Or</p>
</div>
<!-- Username input -->
<div class="form-outline mb-4">
<input type="text" name="username" class="form-control form-control-lg"
placeholder="Username" />
</div>
<!-- Password input -->
<div class="form-outline mb-3">
<input type="password" name="password" class="form-control form-control-lg"
placeholder="Password" />
</div>
<div class="text-center text-lg-start mt-4 pt-2">
<button type="submit" class="btn btn-primary btn-lg"
style="padding-left: 2.5rem; padding-right: 2.5rem;">Login</button>
</div>
</form>
</div>
</div>
</div>
</section>
</body>
</html>



Controller:


LoginController.java

package com.henry.springsecurity.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class LoginController {

@GetMapping("/login")
public String login() {
return "login";
}
}


WebController.java


package com.henry.springsecurity.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Optional;

@RestController
public class WebController {


@GetMapping("/")
public String publicPage(){
return "Hello Henry";
}

@GetMapping("/private")
public String privatePage(Authentication authentication){
return "Welcome " + getName(authentication);
}

private String getName(Authentication authentication) {
return Optional.of(authentication.getPrincipal())
.filter(OidcUser.class::isInstance)
.map(OidcUser.class::cast)
.map(OidcUser::getEmail)
.orElseGet(authentication::getName);

}
}



Project Dependencies:


pom.xml


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.henry</groupId>
<artifactId>spring-security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-security</name>
<description>Demo project for Spring Boot Security</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>


application.yml


spring:
security:
oauth2:
client:
registration:
google:
client-id: YOUR_CLIENT_ID
client-secret: YOUR_CLIENT_SECRET


Run & Test


Run Spring Boot application with command: mvn spring-boot:run. by console, IntelliJ etc.

  • Without Security go to: http://localhost:8080/ result this:







  • With Security go to: http://localhost:8080/private result this:












Type:

User: henry 
Password: password 


Note: In the case, we will be test with admin user,  you can typing anything password, because I created the  AdminAuthenticateProvider class and configure in Spring Security config authenticationProvider. for more details check in the end the references.


  • Test 1 - With user and password result this





  • Test 2 - With  OAuth2 Login with Google result this





  • Test 3 - With Basic Auth

  • Test 4 - With admin user typing anything password.






















Source Code


Here on GitHub.




References.

https://docs.spring.io/spring-security/reference/index.html
https://docs.spring.io/spring-security/reference/features/authentication/index.html



Multiple Data Sources in Spring Boot 3 with Java 21

  In this blog post, we'll explore the configuration and setup for a Spring Boot 3 application with Java 21 that uses multiple data sour...