Skip to content

Commit

Permalink
[Hexlet#159] add authentication with private email
Browse files Browse the repository at this point in the history
  • Loading branch information
d1z3d committed Aug 17, 2024
1 parent cba7a52 commit 03b773b
Show file tree
Hide file tree
Showing 12 changed files with 116 additions and 66 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
runtimeOnly("org.springframework.boot:spring-boot-devtools")
// Thymeleaf
implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE")
Expand All @@ -48,7 +49,6 @@ dependencies {
implementation("org.mapstruct:mapstruct:1.5.3.Final")
// Annotation processors
annotationProcessor("org.mapstruct:mapstruct-processor:1.5.3.Final")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
// Testing
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
Expand Down
25 changes: 9 additions & 16 deletions src/main/java/io/hexlet/typoreporter/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package io.hexlet.typoreporter.config;

import io.hexlet.typoreporter.config.oauth2.OAuth2ConfigurationProperties;
import io.hexlet.typoreporter.handler.OAuth2SuccessHandler;
import io.hexlet.typoreporter.handler.exception.ForbiddenDomainException;
import io.hexlet.typoreporter.handler.exception.WorkspaceNotFoundException;
import io.hexlet.typoreporter.security.service.AccountDetailService;
import io.hexlet.typoreporter.security.service.CustomOAuth2UserService;
import io.hexlet.typoreporter.security.service.SecuredWorkspaceService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand All @@ -28,12 +26,13 @@
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.context.DelegatingSecurityContextRepository;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.annotation.RequestScope;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
Expand All @@ -46,8 +45,6 @@
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private CustomOAuth2UserService oAuth2UserService;
@Autowired
private OAuth2ConfigurationProperties oAuth2ConfigurationProperties;

Expand Down Expand Up @@ -111,11 +108,8 @@ public SecurityFilterChain filterChain(HttpSecurity http,
)
.oauth2Login(config -> config
.loginPage("/login")
.userInfoEndpoint()
.userService(oAuth2UserService)
.and()
.clientRegistrationRepository(getClientRegistrationRepository())
.successHandler(getOAuth2SuccessHandler())
.defaultSuccessUrl("/workspaces")
.failureUrl("/login")
)
.csrf(csrf -> csrf
.ignoringRequestMatchers(
Expand All @@ -140,15 +134,18 @@ private ClientRegistration githubClientRegistration() {
return CommonOAuth2Provider.GITHUB.getBuilder("github")
.clientId(oAuth2ConfigurationProperties.getClientId())
.clientSecret(oAuth2ConfigurationProperties.getClientSecret())
.redirectUri(oAuth2ConfigurationProperties.getRedirectUri())
.scope(oAuth2ConfigurationProperties.getScope())
.build();
}

@Bean
public AuthenticationSuccessHandler getOAuth2SuccessHandler() {
return new OAuth2SuccessHandler();
@RequestScope
public RestTemplate getRestTemplate() {
return new RestTemplate();
}


@Bean
public AccessDeniedHandler accessDeniedHandler() {
return new CustomAccessDeniedHandler();
Expand Down Expand Up @@ -179,10 +176,6 @@ protected void doFilterInternal(HttpServletRequest request,
response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
} catch (WorkspaceNotFoundException e) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("principalNull")) {
response.sendRedirect("/login");
}
}
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import java.security.Principal;
import java.util.Optional;

import static org.springframework.security.core.context.SecurityContextHolder.getContext;

@Configuration
@EnableJpaAuditing
public class AuditConfiguration {

@Bean
public AuditorAware<String> auditorAware() {
return () -> Optional.ofNullable(getContext().getAuthentication()).map(Principal::getName);
return () -> Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.filter(Authentication::isAuthenticated)
.map(Principal::getName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.HashSet;

@Configuration
@ConfigurationProperties(prefix = "spring.security.oauth2.client.registration.github")
Expand All @@ -18,6 +18,8 @@ public class OAuth2ConfigurationProperties {
@Value("clientSecret")
private String clientSecret;
@Value("scope")
private List<String> scope;
private HashSet<String> scope;
@Value("redirect-uri")
private String redirectUri;
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.hexlet.typoreporter.domain.account;

import io.hexlet.typoreporter.service.dto.oauth2.PrivateEmail;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;

Expand All @@ -9,9 +10,11 @@

public class CustomOAuth2User implements OAuth2User {
private final OAuth2User oAuth2User;
private final PrivateEmail privateEmail;

public CustomOAuth2User(OAuth2User oAuth2User) {
public CustomOAuth2User(OAuth2User oAuth2User, PrivateEmail email) {
this.oAuth2User = oAuth2User;
this.privateEmail = email;
}

@Override
Expand All @@ -26,15 +29,16 @@ public Collection<? extends GrantedAuthority> getAuthorities() {

@Override
public String getName() {
return oAuth2User.getAttribute("email");
return this.privateEmail.getEmail();
}

public String getEmail() {
return oAuth2User.getAttribute("email");
return this.privateEmail.getEmail();
}
public String getLogin() {
return oAuth2User.getAttribute("login");
}
//TODO: fix required sets first and last names after issue #286 will be done (empty names)
public String getFirstName() {
String[] fullName = Objects.requireNonNull(oAuth2User.<String>getAttribute("name")).split(" ");
return fullName[1];
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.hexlet.typoreporter.handler.exception;

import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.web.ErrorResponseException;

public class OAuth2Exception extends ErrorResponseException {
public OAuth2Exception(HttpStatusCode status, ProblemDetail body, Throwable cause) {
super(status, body, cause);
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
package io.hexlet.typoreporter.security.service;

import io.hexlet.typoreporter.domain.account.CustomOAuth2User;
import io.hexlet.typoreporter.service.AccountService;
import io.hexlet.typoreporter.service.GithubService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
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;

@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
private final GithubService githubService;
private final AccountService accountService;

public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User user = super.loadUser(userRequest);
return new CustomOAuth2User(user);
var email = githubService.getPrivateEmail(userRequest.getAccessToken().getTokenValue());
var customUser = new CustomOAuth2User(user, email);
Authentication authentication = new UsernamePasswordAuthenticationToken(
customUser, customUser.getPassword(), customUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
accountService.processOAuthPostLogin(customUser);

return customUser;
}
}
10 changes: 4 additions & 6 deletions src/main/java/io/hexlet/typoreporter/service/AccountService.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,11 @@ public Account updatePassword(final UpdatePassword updatePassword, final String
public void processOAuthPostLogin(CustomOAuth2User user) {
var existUser = accountRepository.existsByEmail(user.getEmail());
if (!existUser) {
Account account = new Account();
account.setEmail(user.getEmail());
SignupAccount signupAccount = new SignupAccount(
user.getLogin(), user.getEmail(),
passwordEncoder.encode(user.getPassword()), user.getFirstName(), user.getLastName());
Account account = accountMapper.toAccount(signupAccount);
account.setAuthProvider(AuthProvider.GITHUB);
account.setUsername(user.getLogin());
account.setPassword(passwordEncoder.encode(user.getPassword()));
account.setFirstName(user.getFirstName());
account.setLastName(user.getLastName());
accountRepository.save(account);
}
}
Expand Down
41 changes: 41 additions & 0 deletions src/main/java/io/hexlet/typoreporter/service/GithubService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.hexlet.typoreporter.service;

import io.hexlet.typoreporter.handler.exception.OAuth2Exception;
import io.hexlet.typoreporter.service.dto.oauth2.PrivateEmail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;

@Service
public class GithubService {
@Autowired
private RestTemplate restTemplate;
private static final String GITHUB_API_USER_PRIVATE_EMAILS = "https://api.github.com/user/emails";

public PrivateEmail getPrivateEmail(String accessToken) {
if (accessToken.isBlank()) {
throw new OAuth2Exception(HttpStatus.FORBIDDEN, ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN,
"Access token is not valid. Token is: " + accessToken), null);
}
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
var response = restTemplate.exchange(
GITHUB_API_USER_PRIVATE_EMAILS, HttpMethod.GET, requestEntity, PrivateEmail[].class);
if (response.getStatusCode() != HttpStatus.OK) {
throw new OAuth2Exception(HttpStatus.UNAUTHORIZED, ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED,
"HTTP code response is not 200. Code: " + response.getStatusCode()), null);
}
return Arrays.stream(response.getBody())
.filter(PrivateEmail::isPrimary)
.findFirst()
.orElseThrow(() -> new RuntimeException("no available email"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.hexlet.typoreporter.service.dto.oauth2;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class PrivateEmail {
private String email;
private boolean verified;
private boolean primary;
private String visibility;
}
1 change: 1 addition & 0 deletions src/main/resources/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ spring:
github:
clientId: Ov23liMZqO6eA0FyjeM4
clientSecret: 3948c7b7ac39d2ee6611e611259c9422cdf00f96
redirect-uri: "{baseUrl}/workspaces"
scope:
- user:email
- read:user

0 comments on commit 03b773b

Please sign in to comment.