Java Spring Boot (Part 6)

OAuth2 Login

Spring Framework logo by Pivotal Software is licensed under Apache License 2.0

OAuth2 Login

OAuth2 authentication using Spring Security and GitHub.

I used spring initializr to create a new project with a dependency on Spring Web and OAuth2 Client

I made a small change to the SpringOauth2Application.java file (see above) and ran the project.
The project generated a temporary password

I accessed the running code on http://localhost:8080 and I was redirected to a login page

I entered the username "user" and the generated a temporary password and the home page was displayed

GitHub

To switch to using GitHub as an OAuth2 authentication service I would need a github.client-id and a github.client-secret.

I logged into my GitHub account and clicked on the Settings menu item

I clicked on the Developer settings menu item

I selected the existing localhost OAuth application (if it had not already existed I would have created it)

I updated the Authorization callback URL and took a note of the Client ID and a newly generated Client Secret

I added the github.client-id and a github.client-secret values to my application.properties file

Now when I tried to access http://localhost:8080 I was redirected to a GitHub page

I provided the Authentication code from my two-factor authentication app

I was redirected back to the http://localhost:8080 home page

Adding Views and Roles

I added an admin, user and index view.
I added a USER role and an ADMIN role

I added a Thymeleaf dependency

I created a SecurityFilterChain Bean to control access

SecurityConfig.java
         
package com.haddley.springoauth2.controller.config;

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.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http.authorizeRequests(auth -> auth.requestMatchers("/").permitAll());
        http.authorizeRequests(auth -> auth.requestMatchers("/webjars/bootstrap/**", "/webjars/jquery/**", "/webjars/popper.js/**").permitAll());
        http.authorizeRequests(auth -> auth.requestMatchers("/user").hasRole("USER"));
        http.authorizeRequests(auth -> auth.requestMatchers("/admin").hasRole("ADMIN"));
        http.authorizeRequests(auth -> auth.requestMatchers("/api/v1/user").hasRole("USER"));
        http.authorizeRequests(auth -> auth.requestMatchers("/api/v1/admin").hasRole("ADMIN"));

        return http.oauth2Login().and().build();
    }
}
WebConfig.java
         
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    private static final String WEBJARS_PATH_PATTERN = "/webjars/**";
    private static final String WEBJARS_LOCATION = "/webjars/";

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler(WEBJARS_PATH_PATTERN)
                .addResourceLocations(WEBJARS_LOCATION);
    }
}

I created a service that assigns security Roles to users (user with GitHub ID=15018162 is the only ADMIN)

CustomOAuthUserService.java
         
package com.haddley.springoauth2.service;

import com.haddley.springoauth2.model.CustomOAuth2User;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    private static final Logger logger = LoggerFactory.getLogger(CustomOAuth2UserService.class);
    private static final String ADMIN_USER_NAME = "15018162";
    private static final String ROLE_USER = "ROLE_USER";
    private static final String ROLE_ADMIN = "ROLE_ADMIN";
    private static final String GITHUB_REGISTRATION_ID = "github";

    @Override
    public CustomOAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User user = super.loadUser(userRequest);

        logger.info("User Details: {}", user);
        logger.info("User Name Details: {}", user.getName());

        if (!GITHUB_REGISTRATION_ID.equals(userRequest.getClientRegistration().getRegistrationId())) {
            throw new OAuth2AuthenticationException("ID is not from GitHub");
        }

        return new CustomOAuth2User(user, getAuthorities(user));
    }

    private List<SimpleGrantedAuthority> getAuthorities(OAuth2User user) {
        if (ADMIN_USER_NAME.equals(user.getName())) {
            return Arrays.asList(new SimpleGrantedAuthority(ROLE_USER), new SimpleGrantedAuthority(ROLE_ADMIN));
        }

        return Collections.singletonList(new SimpleGrantedAuthority(ROLE_USER));
    }
}
CustomOAuth2User.java
         
package com.haddley.springoauth2.model;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;

public class CustomOAuth2User implements OAuth2User {

    private final OAuth2User oAuth2User;
    private final Collection<? extends GrantedAuthority> authorities;

    public CustomOAuth2User(OAuth2User oAuth2User, Collection<? extends GrantedAuthority> authorities) {
        this.oAuth2User = oAuth2User;
        this.authorities = Collections.unmodifiableCollection(authorities);
    }

    @Override
    public Map<String, Object> getAttributes() {
        return oAuth2User.getAttributes();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getName() {
        return oAuth2User.getName();
    }
}

REST Controller (not used here because we are generating pages server-side only)

MyRestController.java
         
package com.haddley.springoauth2.controller;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.security.core.Authentication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestController
public class MyRestController {

    private static final Logger logger = LoggerFactory.getLogger(MyRestController.class);
    private static final String DEFAULT_NAME = "World";
    private static final String SECURED_MESSAGE_FORMAT = "Secured %s!";

    @GetMapping("api/v1/user")
    public String userHello(@RequestParam(value = "name", defaultValue = DEFAULT_NAME) String name, Authentication authentication) {
        logger.info("Authentication Details: {}", authentication);
        return String.format(SECURED_MESSAGE_FORMAT, name);
    }

    @GetMapping("api/v1/admin")
    public String adminHello(@RequestParam(value = "name", defaultValue = DEFAULT_NAME) String name, Authentication authentication) {
        logger.info("Authentication Details: {}", authentication);
        return String.format(SECURED_MESSAGE_FORMAT, name);
    }
}

ModelAndView Controller

MyPageController.java
         
package com.haddley.springoauth2.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.security.core.Authentication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.Map;

@Controller
public class MyPageController {

    private static final Logger logger = LoggerFactory.getLogger(MyPageController.class);
    private static final String AVATAR_URL_KEY = "avatar_url";
    private static final String INDEX_VIEW_NAME = "index";
    private static final String USER_VIEW_NAME = "user";
    private static final String ADMIN_VIEW_NAME = "admin";

    @GetMapping("/")
    public ModelAndView index(Authentication authentication) {
        return prepareModelAndView(authentication, INDEX_VIEW_NAME);
    }

    @GetMapping("/user")
    public ModelAndView user(Authentication authentication) {
        return prepareModelAndView(authentication, USER_VIEW_NAME);
    }

    @GetMapping("/admin")
    public ModelAndView admin(Authentication authentication) {
        return prepareModelAndView(authentication, ADMIN_VIEW_NAME);
    }

    private ModelAndView prepareModelAndView(Authentication authentication, String viewName) {
        logger.info("Authentication Details: {}", authentication);

        ModelAndView modelAndView = new ModelAndView(viewName);
        boolean isAuthenticated = false;
        boolean isUser = false;
        boolean isAdmin = false;

        if (authentication != null && authentication.isAuthenticated()) {
            OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
            String avatarUrl = getAvatarUrl(oAuth2User);

            isAuthenticated = true;
            isUser = authentication.getAuthorities().stream().anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_USER"));
            isAdmin = authentication.getAuthorities().stream().anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_ADMIN"));

            logger.info("User Avatar URL: {}", avatarUrl);

            modelAndView.addObject("avatarUrl", avatarUrl);

        }

        logger.info("isAuthenticated: {}", isAuthenticated);
        logger.info("isUser: {}", isUser);
        logger.info("isAdmin: {}", isAdmin);

        modelAndView.addObject("isAuthenticated", isAuthenticated);
        modelAndView.addObject("isUser", isUser);
        modelAndView.addObject("isAdmin", isAdmin);

        return modelAndView;
    }

    private String getAvatarUrl(OAuth2User oAuth2User) {
        Map<String, Object> attributes = oAuth2User.getAttributes();
        return (String) attributes.get(AVATAR_URL_KEY);
    }
}

index view

index.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>My Application</title> <script th:src="@"></script> <script th:src="@"></script> <link rel="stylesheet" type="text/css" th:href="@" /> <link rel="stylesheet" type="text/css" th:href="@" /> </head> <body> <div th:replace="navbar.html :: navbar(page='index')"></div> <div> <h1> Home Page </h1> </div> </body> </html>

navbar fragment

navbar.html
<nav th:fragment="navbar (page)" class="navbar navbar-expand-md bg-dark navbar-dark"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" th:href="@">My Application</a> </div> <ul class="nav navbar-nav"> <li th:if="$"><a class="nav-link" th:href="@">Login</a></li> <li th:if="$"><a class="active nav-link" th:href="@">User</a></li> <li th:if="$"><a class="nav-link" th:href="@">User</a></li> <li th:if="$"><a class="active nav-link" th:href="@">Admin</a></li> <li th:if="$"><a class="nav-link" th:href="@">Admin</a></li> <li th:if="$"> <img th:src="$" style="height: 40px; width: 40px;" class="avatar circle"> </li> </ul> </div> </nav>
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.1.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.haddley</groupId>
	<artifactId>spring-oauth2</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-oauth2</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-oauth2-client</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

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

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


		<dependency>
    		<groupId>org.webjars</groupId>
    		<artifactId>bootstrap</artifactId>
    		<version>5.3.2</version>
		</dependency>
		<dependency>
    		<groupId>org.webjars</groupId>
    		<artifactId>jquery</artifactId>
    		<version>3.1.1</version>
		</dependency>


	</dependencies>

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

</project>

Home view

I clicked on the Login button

I entered an Authentication code

Home view with User Menu Item, Admin Menu Item and User Avatar.

User view

Admin view

user api result

admin api result