Java Spring Boot (Part 6)
Neil Haddley โข October 31, 2023
Signing in with GitHub OAuth2 using Spring Security, then using roles to control who can open the user and admin pages
I implemented OAuth2 authentication using Spring Security and GitHub.
To switch to using GitHub as an OAuth2 authentication service I needed a github.client-id and a github.client-secret.
Under the hood: What happened during the sign-in
The screenshots show the steps in order. I asked for the home page, and the application redirected me to a GitHub page. I proved who I was to GitHub, including the code from my two-factor authentication app. GitHub then redirected me back to the callback URL in my application, and the home page appeared. Spring Security handled the redirects, and the only things that I had to supply were the client id, the client secret and the callback URL.
Adding Views and Roles
I added an admin, user and index view.
I added a USER role and an ADMIN role
SecurityConfig.java
JAVA
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
JAVA
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
JAVA
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
JAVA
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
JAVA
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
JAVA
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
XML
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>


























