Java Spring Boot (Part 6)
Neil Haddley • October 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 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
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

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)

ModelAndView Controller

index view

navbar fragment

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