Duplicate Oauth2 AccessTokens Created

Background

솔루션 내 OAuth2 기능 중에 다음과 같은 오류가 간헐적으로 발생한다고 연락을 받았습니다.

생략...
Incorrect result size: expected 1, actual 2
...
...JdbcTemplate.queryForObject(JdbcTemplate.java:797)
...JdbcTokenStore.getAccessToken(JdbcTokenStore.java:105)
...JadeTokenService.createAccessToken(JadeTokenService.java:112)
생략...

CheckPoint

OAuth2는 Spring의 OAuth2 기능을 활용하였기 때문에, 관련 내용이 있는지 먼저 검색을 하였고, 아래와 같이 동일한 이슈가 있다는 것을 확인하였습니다.

https://github.com/spring-projects/spring-security-oauth/issues/276
https://github.com/jhipster/generator-jhipster/issues/1759
https://github.com/jhipster/generator-jhipster/pull/1782

위의 이슈들을 종합해본 결과, 첫번째로 Spring OAuth2 Old 버전에서는 트랜잭션 처리가 되고 있지 않을 수 있다는 것과 두번째로 동일한 client_id로 동시에 여러건의 token을 요청한다면 2건 이상의 동일한 token이 DB에 저장될 수 있다는 것을 확인할 수 있었습니다.
하지만, 어플리케이션 단에서 해당 Token을 꺼낼때는 1개의 값만 기대하고 꺼내기 때문에 2건이상 조회가 되면 바로 오류가 발생할 수 있도록 되어 있습니다.

Replayed

오류나 버그 조치에 가장 중요한 일은 프로젝트팀에서 발생한 현상을 동일하게 재연하는 것 입니다.

PostMan과 Fillder를 활용하여 동일한 요청을 복사하여 동시에 여러건을 호출함으로써 동일한 현상을 재연하였습니다.

  • PostMan을 활용한 토큰 발급요청

  • Fiddler를 활용한 다중건 요청. 500에러 발생 확인.

  • 500 Error 상세내용

14:23:35.760 [http-bio-8080-exec-8] ERROR        s.e.SpringWebExceptionHandler [EID:] [SS-ID:] [USER-ID:] - Incorrect result size: expected 1, actual 3[END]
org.springframework.dao.IncorrectResultSizeDataAccessException: Incorrect result size: expected 1, actual 3
	at org.springframework.dao.support.DataAccessUtils.requiredSingleResult(DataAccessUtils.java:74)
	at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:737)
	at org.springframework.security.oauth2.provider.token.JdbcTokenStore.getAccessToken(JdbcTokenStore.java:113)
	at 
	...
  • 테이블에 동일한 토큰이 3건 추가된 내용 확인.

Task

위와 같이 해당 현상확인 및 원인을 파악하였으니, 아래와 같이 조치를 진행하도록 합니다.

Transaction 처리

우선, JNDI 사용 시 WAS의 autoCommit 속성이 false인 경우 트랜잭션 처리가 되어 있지 않으면 commit이 수행되지 않을 수 있습니다. 이러한 케이스도 고려하여, 트랜잭션 애노테이션(@Transactional)을 추가하여 트랜잭션 종료 후 commit이 되도록 합니다.

public class JadeTokenService ... {
	...
	@Transacrional
	public OAuth2AccessToken createAccessToken(...) throws AuthenticationException { ... }

	@Transacrional
	public OAuth2AccessToken refreshAccessToken(...) { ... }
	...
}

PrimaryKey 추가 및 DuplicateException 처리

  • PrimaryKey 추가
    AUTHENTICATION_ID를 PK로 정의합니다.
    /* 발급된 접근 토큰 */
    CREATE TABLE OAUTH_ACCESS_TOKEN (
    	TOKEN_ID VARCHAR2(256), /* 토큰 아이디 */
    	TOKEN BLOB, /* 토큰 */
    	AUTHENTICATION_ID VARCHAR2(256) NOT NULL, /* 인증 아이디 */
    	USER_NAME VARCHAR2(256), /* 사용자 명 */
    	CLIENT_ID VARCHAR2(256), /* 클라이언트 아이디 */
    	AUTHENTICATION BLOB, /* 인증 정보 */
    	REFRESH_TOKEN VARCHAR2(256) /* 리프레시 토큰 */
    );
    CREATE UNIQUE INDEX PK_OAUTH_ACCESS_TOKEN
    	ON OAUTH_ACCESS_TOKEN (
    		AUTHENTICATION_ID ASC
    );
    ALTER TABLE OAUTH_ACCESS_TOKEN
    	ADD
    		CONSTRAINT PK_OAUTH_ACCESS_TOKEN
    		PRIMARY KEY (
    			AUTHENTICATION_ID
    		);
  • DuplicateException 처리
    DuplicateException 발생 시, Token을 조회하도록 처리합니다.
    // https://github.com/spring-projects/spring-security-oauth/issues/502
    public class EnsureTokenService extends JadeTokenService {
    	
    	private static final Log LOG = LogFactory.getLog(EnsureTokenService.class);
    	
    	@Override
    	@Transactional
    	public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
    		try {
              return super.createAccessToken(authentication);
          }catch (DuplicateKeyException dke) {
          	LOG.info(String.format("Duplicate Primary Key(Authenticate ID) found for %s",authentication.getUserAuthentication().getPrincipal()));
              return super.getAccessToken(authentication);
          }catch (Exception ex) {
          	LOG.info(String.format("Exception while creating access token %s",ex));
          }
          return null;
    	}
    }

좋은 웹페이지 즐겨찾기