Skip to content

Commit

Permalink
Feature/#129 social login (#158)
Browse files Browse the repository at this point in the history
* feat: naver 추가

* feat: Account memberId nullable

* feat: LocalSecurityConfig 분리

* feat: access token, id token 로그인

* prune: 기존 id token 로그인 제거

* feat: 회원가입 수정

account memberid nullable 반영, jwt 제거

* docs: 토큰 로그인

* prune: usernameservice remove
  • Loading branch information
Cho-D-YoungRae authored Jan 14, 2024
1 parent 07940ce commit f16be69
Show file tree
Hide file tree
Showing 21 changed files with 367 additions and 351 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.cmc.curtaincall.domain.account;

import jakarta.persistence.AttributeOverride;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
Expand All @@ -13,6 +14,7 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.cmc.curtaincall.domain.account.exception.AccountAlreadySignupException;
import org.cmc.curtaincall.domain.core.BaseTimeEntity;
import org.cmc.curtaincall.domain.member.MemberId;

Expand All @@ -38,6 +40,10 @@ public class Account extends BaseTimeEntity {
private String username;

@Embedded
@AttributeOverride(
name = "id",
column = @Column(name = "member_id")
)
private MemberId memberId;

@Builder
Expand All @@ -46,4 +52,15 @@ public Account(final String username, final MemberId memberId) {
this.memberId = memberId;
}

public Account(final String username) {
this.username = username;
}

public void signup(final MemberId memberId) {
if (getMemberId() != null) {
throw new AccountAlreadySignupException(getUsername());
}
this.memberId = memberId;
}

}
4 changes: 2 additions & 2 deletions web/src/docs/asciidoc/authenticate.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ operation::auth-login-page[snippets='http-request,path-parameters']
소셜 로그인 별 로그인 웹 페이지 응답


[[security-login]]
[[security-token-login]]
== 커튼콜 로그인

operation::security-login[snippets='http-request,request-headers,http-response,response-fields']
operation::security-token-login[snippets='http-request,path-parameters,request-fields,http-response,response-fields']


[[account-get-user-member]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.cmc.curtaincall.web.security.service.CurtainCallJwtEncoderService;
import org.cmc.curtaincall.web.security.service.UsernameService;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
Expand Down Expand Up @@ -51,9 +50,4 @@ public CurtainCallJwtEncoderService curtainCallJwtEncoderService(
CurtainCallJwtProperties properties, JwtEncoder curtainCallJwtEncoder) {
return new CurtainCallJwtEncoderService(curtainCallJwtEncoder, properties.getAccessTokenValidity());
}

@Bean
public UsernameService usernameService(OAuth2ClientProperties properties) {
return new UsernameService(properties);
}
}
Original file line number Diff line number Diff line change
@@ -1,77 +1,76 @@
package org.cmc.curtaincall.web.security.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.cmc.curtaincall.domain.account.dao.AccountDao;
import org.cmc.curtaincall.domain.account.repository.AccountRepository;
import org.cmc.curtaincall.web.security.service.CurtainCallJwtEncoderService;
import org.cmc.curtaincall.web.security.service.UsernameService;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Configuration
@EnableConfigurationProperties(OAuth2ClientProperties.class)
public class OAuth2LoginConfig {

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
@Profile("local")
public SecurityFilterChain oauth2LoginSecurityFilterChain(
HttpSecurity http,
OAuth2LoginAuthenticationSuccessHandler oAuth2LoginAuthenticationSuccessHandler
public SecurityFilterChain accessTokenSecurityFilterChain(
final HttpSecurity http,
final OAuth2TokenLoginAuthenticationFilter authenticationFilter
) throws Exception {
return http.securityMatcher("/oauth2/**", "/login/oauth2/**")
return http.securityMatcher("/login/oauth2/token/*")
.csrf(csrf -> csrf.disable())
.formLogin(formLogin -> formLogin.disable())
.httpBasic(httpBasic -> httpBasic.disable())
.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.disable())
.oauth2Login(oauth2Login -> oauth2Login.disable())
.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.oauth2Login(oauth2Login -> oauth2Login
.successHandler(oAuth2LoginAuthenticationSuccessHandler)
)
.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.disable())
.addFilterAt(authenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}

@Bean
@Profile("local")
public OAuth2LoginAuthenticationSuccessHandler oAuth2LoginAuthenticationSuccessHandler(
final ObjectMapper objectMapper,
final CurtainCallJwtEncoderService jwtEncoderService,
final UsernameService usernameService,
final AccountDao accountDao) {
return new OAuth2LoginAuthenticationSuccessHandler(
objectMapper, jwtEncoderService, usernameService, accountDao);
}
public OAuth2TokenLoginAuthenticationFilter oAuth2TokenLoginAuthenticationFilter(
final JwtIssuerAuthenticationManagerResolver authenticationManagerResolver,
final ClientRegistrationRepository clientRegistrationRepository,
final CurtainCallJwtEncoderService curtainCallJwtEncoderService,
final AccountRepository accountRepository,
final OAuth2ClientProperties properties,
final ObjectMapper objectMapper
) {
final OAuth2TokenLoginAuthenticationSuccessHandler authenticationSuccessHandler = new OAuth2TokenLoginAuthenticationSuccessHandler(
objectMapper, curtainCallJwtEncoderService, accountRepository
);

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain accessTokenSecurityFilterChain(
HttpSecurity http,
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver
) throws Exception {
return http.securityMatcher("/login", "/signup")
.csrf(csrf -> csrf.disable())
.formLogin(formLogin -> formLogin.disable())
.httpBasic(httpBasic -> httpBasic.disable())
.oauth2Login(oauth2Login -> oauth2Login.disable())
.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer
.authenticationManagerResolver(authenticationManagerResolver)
)
.build();
final Map<String, String> issuerUriToProviderName = properties.getProvider().entrySet().stream()
.filter(entry -> entry.getValue().getIssuerUri() != null)
.collect(Collectors.toMap(entry -> entry.getValue().getIssuerUri(), Map.Entry::getKey));
final OAuth2TokenLoginAuthenticationFilter authenticationFilter = new OAuth2TokenLoginAuthenticationFilter(
authenticationManagerResolver,
clientRegistrationRepository,
issuerUriToProviderName,
objectMapper
);
authenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
return authenticationFilter;
}

@Bean
Expand All @@ -84,4 +83,21 @@ public JwtIssuerAuthenticationManagerResolver authenticationManagerResolver(
return new JwtIssuerAuthenticationManagerResolver(trustedIssuers);
}

static class OAuth2TokenLoginConfigurer<B extends HttpSecurityBuilder<B>>
extends AbstractAuthenticationFilterConfigurer<B, OAuth2TokenLoginConfigurer<B>, OAuth2TokenLoginAuthenticationFilter> {

@Override
public void init(final B http) throws Exception {
// final OAuth2TokenLoginAuthenticationFilter authFilter = new OAuth2TokenLoginAuthenticationFilter();
// authFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
// loginProcessingUrl(OAuth2TokenLoginAuthenticationFilter.FILTER_PROCESSES_URI);
// setAuthenticationFilter(authFilter);
}

@Override
protected RequestMatcher createLoginProcessingUrlMatcher(final String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.cmc.curtaincall.web.security.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.cmc.curtaincall.web.security.request.TokenLoginRequest;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;

import java.io.IOException;
import java.util.Map;

public class OAuth2TokenLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

public static final String FILTER_PROCESSES_URI = "/login/oauth2/token/*";

private final AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;

private final ClientRegistrationRepository clientRegistrationRepository;

private final Map<String, String> issuerToProvider;

private final ObjectMapper objectMapper;

private final OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = new DefaultOAuth2UserService();

public OAuth2TokenLoginAuthenticationFilter(
final AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver,
final ClientRegistrationRepository clientRegistrationRepository,
final Map<String, String> issuerToProvider,
final ObjectMapper objectMapper
) {
super(FILTER_PROCESSES_URI);
setAuthenticationManager(authentication -> {
throw new AuthenticationServiceException("Cannot authenticate " + authentication);
});
this.authenticationManagerResolver = authenticationManagerResolver;
this.clientRegistrationRepository = clientRegistrationRepository;
this.issuerToProvider = issuerToProvider;
this.objectMapper = objectMapper;
}

@Override
public Authentication attemptAuthentication(
final HttpServletRequest request, final HttpServletResponse response
) throws AuthenticationException, IOException, ServletException {
final String provider = request.getServletPath().split("/")[4];
final TokenLoginRequest loginRequest = objectMapper.readValue(request.getReader(), TokenLoginRequest.class);
if ("naver".equals(provider)) {
final ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(provider);
final OAuth2User oAuth2User = oauth2UserService.loadUser(new OAuth2UserRequest(clientRegistration, new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, loginRequest.token(), null, null
)));
return UsernamePasswordAuthenticationToken.authenticated(
provider + "-" + oAuth2User.getName(), null, AuthorityUtils.NO_AUTHORITIES
);
} else {
final BearerTokenAuthenticationToken authenticationToken = new BearerTokenAuthenticationToken(
loginRequest.token());
final AuthenticationManager authenticationManager = authenticationManagerResolver.resolve(request);
final JwtAuthenticationToken authentication = (JwtAuthenticationToken) authenticationManager
.authenticate(authenticationToken);
final String issuer = (String) authentication.getTokenAttributes().get(JwtClaimNames.ISS);
if (!provider.equals(issuerToProvider.get(issuer))) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
}

return UsernamePasswordAuthenticationToken.authenticated(
provider + "-" + authentication.getName(), null, AuthorityUtils.NO_AUTHORITIES
);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.cmc.curtaincall.web.security.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.cmc.curtaincall.domain.account.Account;
import org.cmc.curtaincall.domain.account.repository.AccountRepository;
import org.cmc.curtaincall.domain.member.MemberId;
import org.cmc.curtaincall.web.security.response.LoginResponse;
import org.cmc.curtaincall.web.security.service.CurtainCallJwtEncoderService;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Optional;

@RequiredArgsConstructor
public class OAuth2TokenLoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

private final ObjectMapper objectMapper;

private final CurtainCallJwtEncoderService jwtEncoderService;

private final AccountRepository accountRepository;

@Override
public void onAuthenticationSuccess(
final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication
) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());

final String username = authentication.getName();
final Account account = accountRepository.findByUsername(username)
.orElseGet(() -> accountRepository.save(new Account(username)));
final Jwt jwt = jwtEncoderService.encode(username);
final LoginResponse loginResponse = new LoginResponse(
Optional.ofNullable(account.getMemberId()).map(MemberId::getId).orElse(null),
jwt.getTokenValue(),
LocalDateTime.ofInstant(jwt.getExpiresAt(), ZoneId.systemDefault())
);
objectMapper.writeValue(response.getWriter(), loginResponse);
}
}
Loading

0 comments on commit f16be69

Please sign in to comment.