Java Spring Boot (Part 6)

Neil HaddleyOctober 31, 2023

GitHub

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 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 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 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

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

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 logged into my GitHub account and clicked on the Settings menu item

I clicked on the Developer 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 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 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

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

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 provided the Authentication code from my two-factor authentication app

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

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 added a Thymeleaf dependency

I created a SecurityFilterChain Bean to control access

I created a SecurityFilterChain Bean to control access

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

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

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

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

ModelAndView Controller

ModelAndView Controller

index view

index view

navbar fragment

navbar fragment

Home view

Home view

I clicked on the Login button

I clicked on the Login button

I entered an Authentication code

I entered an Authentication code

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

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

User view

User view

Admin view

Admin view

user api result

user api result

admin api result

admin api result

SecurityConfig.java

TEXT
1package com.haddley.springoauth2.controller.config;
2
3import org.springframework.context.annotation.Bean;
4import org.springframework.context.annotation.Configuration;
5import org.springframework.security.config.annotation.web.builders.HttpSecurity;
6import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
7import org.springframework.security.web.SecurityFilterChain;
8
9@Configuration
10@EnableWebSecurity
11public class SecurityConfig {
12
13    @Bean
14    SecurityFilterChain configure(HttpSecurity http) throws Exception {
15        http.authorizeRequests(auth -> auth.requestMatchers("/").permitAll());
16        http.authorizeRequests(auth -> auth.requestMatchers("/webjars/bootstrap/**", "/webjars/jquery/**", "/webjars/popper.js/**").permitAll());
17        http.authorizeRequests(auth -> auth.requestMatchers("/user").hasRole("USER"));
18        http.authorizeRequests(auth -> auth.requestMatchers("/admin").hasRole("ADMIN"));
19        http.authorizeRequests(auth -> auth.requestMatchers("/api/v1/user").hasRole("USER"));
20        http.authorizeRequests(auth -> auth.requestMatchers("/api/v1/admin").hasRole("ADMIN"));
21
22        return http.oauth2Login().and().build();
23    }
24}

WebConfig.java

TEXT
1import org.springframework.context.annotation.Configuration;
2import org.springframework.web.servlet.config.annotation.EnableWebMvc;
3import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
4import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
5
6@Configuration
7@EnableWebMvc
8public class WebConfig implements WebMvcConfigurer {
9
10    private static final String WEBJARS_PATH_PATTERN = "/webjars/**";
11    private static final String WEBJARS_LOCATION = "/webjars/";
12
13    @Override
14    public void addResourceHandlers(ResourceHandlerRegistry registry) {
15        registry.addResourceHandler(WEBJARS_PATH_PATTERN)
16                .addResourceLocations(WEBJARS_LOCATION);
17    }
18}

CustomOAuthUserService.java

TEXT
1package com.haddley.springoauth2.service;
2
3import com.haddley.springoauth2.model.CustomOAuth2User;
4import org.springframework.security.core.authority.SimpleGrantedAuthority;
5import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
6import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
7import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
8import org.springframework.security.oauth2.core.user.OAuth2User;
9import org.springframework.stereotype.Service;
10
11import java.util.Arrays;
12import java.util.Collections;
13import java.util.List;
14import org.slf4j.Logger;
15import org.slf4j.LoggerFactory;
16
17@Service
18public class CustomOAuth2UserService extends DefaultOAuth2UserService {
19
20    private static final Logger logger = LoggerFactory.getLogger(CustomOAuth2UserService.class);
21    private static final String ADMIN_USER_NAME = "15018162";
22    private static final String ROLE_USER = "ROLE_USER";
23    private static final String ROLE_ADMIN = "ROLE_ADMIN";
24    private static final String GITHUB_REGISTRATION_ID = "github";
25
26    @Override
27    public CustomOAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
28        OAuth2User user = super.loadUser(userRequest);
29
30        logger.info("User Details: {}", user);
31        logger.info("User Name Details: {}", user.getName());
32
33        if (!GITHUB_REGISTRATION_ID.equals(userRequest.getClientRegistration().getRegistrationId())) {
34            throw new OAuth2AuthenticationException("ID is not from GitHub");
35        }
36
37        return new CustomOAuth2User(user, getAuthorities(user));
38    }
39
40    private List<SimpleGrantedAuthority> getAuthorities(OAuth2User user) {
41        if (ADMIN_USER_NAME.equals(user.getName())) {
42            return Arrays.asList(new SimpleGrantedAuthority(ROLE_USER), new SimpleGrantedAuthority(ROLE_ADMIN));
43        }
44
45        return Collections.singletonList(new SimpleGrantedAuthority(ROLE_USER));
46    }
47}

CustomOAuth2User.java

TEXT
1package com.haddley.springoauth2.model;
2
3import org.springframework.security.core.GrantedAuthority;
4import org.springframework.security.oauth2.core.user.OAuth2User;
5
6import java.util.Collection;
7import java.util.Collections;
8import java.util.Map;
9
10public class CustomOAuth2User implements OAuth2User {
11
12    private final OAuth2User oAuth2User;
13    private final Collection<? extends GrantedAuthority> authorities;
14
15    public CustomOAuth2User(OAuth2User oAuth2User, Collection<? extends GrantedAuthority> authorities) {
16        this.oAuth2User = oAuth2User;
17        this.authorities = Collections.unmodifiableCollection(authorities);
18    }
19
20    @Override
21    public Map<String, Object> getAttributes() {
22        return oAuth2User.getAttributes();
23    }
24
25    @Override
26    public Collection<? extends GrantedAuthority> getAuthorities() {
27        return authorities;
28    }
29
30    @Override
31    public String getName() {
32        return oAuth2User.getName();
33    }
34}

MyRestController.java

TEXT
1package com.haddley.springoauth2.controller;
2
3import org.springframework.web.bind.annotation.RestController;
4import org.springframework.web.bind.annotation.GetMapping;
5import org.springframework.web.bind.annotation.RequestParam;
6import org.springframework.security.core.Authentication;
7import org.slf4j.Logger;
8import org.slf4j.LoggerFactory;
9
10@RestController
11public class MyRestController {
12
13    private static final Logger logger = LoggerFactory.getLogger(MyRestController.class);
14    private static final String DEFAULT_NAME = "World";
15    private static final String SECURED_MESSAGE_FORMAT = "Secured %s!";
16
17    @GetMapping("api/v1/user")
18    public String userHello(@RequestParam(value = "name", defaultValue = DEFAULT_NAME) String name, Authentication authentication) {
19        logger.info("Authentication Details: {}", authentication);
20        return String.format(SECURED_MESSAGE_FORMAT, name);
21    }
22
23    @GetMapping("api/v1/admin")
24    public String adminHello(@RequestParam(value = "name", defaultValue = DEFAULT_NAME) String name, Authentication authentication) {
25        logger.info("Authentication Details: {}", authentication);
26        return String.format(SECURED_MESSAGE_FORMAT, name);
27    }
28}

MyPageController.java

TEXT
1package com.haddley.springoauth2.controller;
2
3import org.springframework.stereotype.Controller;
4import org.springframework.web.bind.annotation.GetMapping;
5import org.springframework.security.core.Authentication;
6import org.slf4j.Logger;
7import org.slf4j.LoggerFactory;
8import org.springframework.web.servlet.ModelAndView;
9import org.springframework.security.oauth2.core.user.OAuth2User;
10
11import java.util.Map;
12
13@Controller
14public class MyPageController {
15
16    private static final Logger logger = LoggerFactory.getLogger(MyPageController.class);
17    private static final String AVATAR_URL_KEY = "avatar_url";
18    private static final String INDEX_VIEW_NAME = "index";
19    private static final String USER_VIEW_NAME = "user";
20    private static final String ADMIN_VIEW_NAME = "admin";
21
22    @GetMapping("/")
23    public ModelAndView index(Authentication authentication) {
24        return prepareModelAndView(authentication, INDEX_VIEW_NAME);
25    }
26
27    @GetMapping("/user")
28    public ModelAndView user(Authentication authentication) {
29        return prepareModelAndView(authentication, USER_VIEW_NAME);
30    }
31
32    @GetMapping("/admin")
33    public ModelAndView admin(Authentication authentication) {
34        return prepareModelAndView(authentication, ADMIN_VIEW_NAME);
35    }
36
37    private ModelAndView prepareModelAndView(Authentication authentication, String viewName) {
38        logger.info("Authentication Details: {}", authentication);
39
40        ModelAndView modelAndView = new ModelAndView(viewName);
41        boolean isAuthenticated = false;
42        boolean isUser = false;
43        boolean isAdmin = false;
44
45        if (authentication != null && authentication.isAuthenticated()) {
46            OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
47            String avatarUrl = getAvatarUrl(oAuth2User);
48
49            isAuthenticated = true;
50            isUser = authentication.getAuthorities().stream().anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_USER"));
51            isAdmin = authentication.getAuthorities().stream().anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_ADMIN"));
52
53            logger.info("User Avatar URL: {}", avatarUrl);
54
55            modelAndView.addObject("avatarUrl", avatarUrl);
56
57        }
58
59        logger.info("isAuthenticated: {}", isAuthenticated);
60        logger.info("isUser: {}", isUser);
61        logger.info("isAdmin: {}", isAdmin);
62
63        modelAndView.addObject("isAuthenticated", isAuthenticated);
64        modelAndView.addObject("isUser", isUser);
65        modelAndView.addObject("isAdmin", isAdmin);
66
67        return modelAndView;
68    }
69
70    private String getAvatarUrl(OAuth2User oAuth2User) {
71        Map<String, Object> attributes = oAuth2User.getAttributes();
72        return (String) attributes.get(AVATAR_URL_KEY);
73    }
74}

pom.xml

TEXT
1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4	<modelVersion>4.0.0</modelVersion>
5	<parent>
6		<groupId>org.springframework.boot</groupId>
7		<artifactId>spring-boot-starter-parent</artifactId>
8		<version>3.1.5</version>
9		<relativePath/> <!-- lookup parent from repository -->
10	</parent>
11	<groupId>com.haddley</groupId>
12	<artifactId>spring-oauth2</artifactId>
13	<version>0.0.1-SNAPSHOT</version>
14	<name>spring-oauth2</name>
15	<description>Demo project for Spring Boot</description>
16	<properties>
17		<java.version>17</java.version>
18	</properties>
19	<dependencies>
20		<dependency>
21			<groupId>org.springframework.boot</groupId>
22			<artifactId>spring-boot-starter-oauth2-client</artifactId>
23		</dependency>
24		<dependency>
25			<groupId>org.springframework.boot</groupId>
26			<artifactId>spring-boot-starter-web</artifactId>
27		</dependency>
28
29		<dependency>
30			<groupId>org.springframework.boot</groupId>
31			<artifactId>spring-boot-starter-test</artifactId>
32			<scope>test</scope>
33		</dependency>
34
35		<dependency>
36			<groupId>org.springframework.boot</groupId>
37			<artifactId>spring-boot-starter-thymeleaf</artifactId>
38		</dependency>
39
40
41		<dependency>
42    		<groupId>org.webjars</groupId>
43    		<artifactId>bootstrap</artifactId>
44    		<version>5.3.2</version>
45		</dependency>
46		<dependency>
47    		<groupId>org.webjars</groupId>
48    		<artifactId>jquery</artifactId>
49    		<version>3.1.1</version>
50		</dependency>
51
52
53	</dependencies>
54
55	<build>
56		<plugins>
57			<plugin>
58				<groupId>org.springframework.boot</groupId>
59				<artifactId>spring-boot-maven-plugin</artifactId>
60			</plugin>
61		</plugins>
62	</build>
63
64</project>