๐Ÿช OAuth 2.0, Http-Only Cookie ํ™œ์šฉ ์ž๋™ ๋กœ๊ทธ์ธ - 1

ozzingยท2022๋…„ 11์›” 5์ผ
0

๋™์•„๋ฆฌ๋ฅผ ํ†ตํ•ด ์ง„ํ–‰ํ•˜์˜€๋˜ ์›น๋ทฐ ํ˜•ํƒœ์˜ ์•ฑ ์„œ๋น„์Šค ํ”„๋กœ์ ํŠธ์—์„œ OAuth 2.0 ํ™œ์šฉํ•œ ์ž๋™ ๋กœ๊ทธ์ธ์„ ๊ตฌํ˜„ํ•˜์˜€๋‹ค. ์นด์นด์˜ค์™€ ์• ํ”Œ ๋ชจ๋‘ REST ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•œ ๋’ค, ์„œ๋น„์Šค ์ž์ฒด JWT ํ† ํฐ์„ ํ†ตํ•ด ์œ ์ € ์ธ์ฆ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์˜€๊ณ  ์ด ๊ณผ์ •์—์„œ Cookie๋ฅผ ํ™œ์šฉํ•ด ์ž๋™ ๋กœ๊ทธ์ธ์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•˜์˜€๋‹ค.

๊ฐ IdP์˜ developers ํŽ˜์ด์ง€์—์„œ ์„ค์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‹ค๋ฅธ ๋ธ”๋กœ๊ทธ์—๋„ ์ž์„ธํžˆ ์ž˜ ์ž‘์„ฑ๋˜์–ด์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ƒ๋žตํ•˜๊ณ ์ž ํ•œ๋‹ค. ์†Œ๊ฐœํ•˜๊ณ ์ž ํ•˜๋Š” ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์นด์นด์˜ค ๋กœ๊ทธ์ธ๊ณผ ์• ํ”Œ ๋กœ๊ทธ์ธ๋งŒ ์‚ฌ์šฉํ•˜์˜€์ง€๋งŒ, ์ถ”๊ฐ€๋กœ ์นด์นด์˜ค ๋กœ๊ทธ์ธ๊ณผ ๋งค์šฐ ์œ ์‚ฌํ•œ ๋„ค์ด๋ฒ„ ๋กœ๊ทธ์ธ๊นŒ์ง€ ์†Œ๊ฐœํ•˜๋ ค๊ณ  ํ•œ๋‹ค.


์นด์นด์˜ค ๋กœ๊ทธ์ธ

1. ์นด์นด์˜ค ์„œ๋ฒ„-ํ”„๋ก ํŠธ์˜ ์ธ๊ฐ€์ฝ”๋“œ ํ†ต์‹ 

ํ”„๋ก ํŠธ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ ์‹œ ์นด์นด์˜ค ์„œ๋ฒ„์—๊ฒŒ ์ธ๊ฐ€์ฝ”๋“œ๋ฅผ ์š”์ฒญํ•œ๋‹ค. ์นด์นด์˜ค ์„œ๋ฒ„๋Š” ์ธ๊ฐ€์ฝ”๋“œ๋ฅผ redirect_uri๋กœ ๋ฐ˜ํ™˜ํ•ด์ฃผ๊ณ , ๊ทธ ํ˜•ํƒœ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. ์•„๋ž˜๋Š” ๋งˆ์Œ๋Œ€๋กœ ์ž‘์„ฑํ•œ ์˜ˆ์‹œ ์ฝ”๋“œ๋‹ค.

"~~/callback?code=kSc9f-ZZ0asdfgafjai4t-D05Tl9zYgas-dfQasdfas3-diw"

์ด ์ฝ”๋“œ๋ฅผ ๋ฐฑ์—”๋“œ๋กœ ๋„˜๊ฒจ์ฃผ๋ฉด ๋œ๋‹ค.

2. ์นด์นด์˜ค ์„œ๋ฒ„-๋ฐฑ์˜ ์œ ์ € ์ •๋ณด ํ†ต์‹ 

๋‘ ๋ฒˆ์˜ ์š”์ฒญ์ด ์˜ค๊ณ  ๊ฐ„๋‹ค.

"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id="
                + KAKAO_KEY + "&redirect_uri=" + KAKAO_URI + "&code="
                + kakaoRequest.getCode()

๋จผ์ € ํ”„๋ก ํŠธ๋กœ๋ถ€ํ„ฐ ๋„˜๊ฒจ ๋ฐ›์€ ์ธ๊ฐ€ ์ฝ”๋“œ๋ฅผ ์œ„์˜ ์ฃผ์†Œ์— POST ์š”์ฒญํ•˜์—ฌ ์ธ๊ฐ€์ฝ”๋“œ์˜ ์œ ํšจ์„ฑ์„ ํ™•์ธ ๋ฐ›๊ณ , ๊ทธ ๋Œ€๊ฐ€๋กœ ์นด์นด์˜ค ์„œ๋ฒ„์˜ Access Token๊ณผ Refresh Token์„ ๋Œ๋ ค ๋ฐ›๋Š”๋‹ค. ์ด ํ† ํฐ๋“ค์€ ํ”„๋ก ํŠธ๋กœ ๋„˜๊ฒจ์ ธ์„œ๋Š” ์•ˆ๋˜๋Š” ์ค‘์š”ํ•œ ์ •๋ณด๋‹ค. ์—ฌ๋Ÿฌ๋ฒˆ ์‹คํ—˜ํ•ด๋ณธ ๊ฒฐ๊ณผ ๋งŒ๋ฃŒ๊ธฐ๊ฐ„์ด ๊ต‰์žฅํžˆ ๊ธด ๋“ฏ ๋ณด์˜€๊ณ , ๋‹ค์Œ ์š”์ฒญ์—๋Š” ํ”„๋กœ์ ํŠธ ๊ณ ์œ ์˜ ๋น„๋ฐ€ ํ‚ค ๋“ฑ์ด ์š”๊ตฌ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

"https://kapi.kakao.com/v2/user/me"

๋‘ ๋ฒˆ์งธ๋กœ ์œ„์—์„œ ๋ฐ›์€ ์นด์นด์˜ค ์„œ๋ฒ„์˜ Access Token๊ณผ Refresh Token๊ณผ ํ•จ๊ป˜ ํ•ด๋‹น ์ฃผ์†Œ์— POST ์š”์ฒญ์„ ๋‚ ๋ ค์•ผ ํ•œ๋‹ค.

์ด ๋•Œ, ์นด์นด์˜ค๋กœ๋ถ€ํ„ฐ ์•„๋ž˜์™€ ๊ฐ™์€ ํ˜•ํƒœ์˜ ์œ ์ € ์ •๋ณด๋ฅผ ๋ฐ›๊ฒŒ ๋œ๋‹ค.

{
    "authenticationCode": "",
    "connectedAt": "",
    "kakaoAccount": {
          "profile": {
				"nickname": ""
          }
    }
}

์œ„์˜ ๋ฆฌ์Šคํฐ์Šค์—์„œ authenticationCode๊ฐ€ uniqueํ•˜๋ฏ€๋กœ ์œ ์ €๋ฅผ ์‹๋ณ„ํ•˜๋Š” ์šฉ๋„๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.


๋„ค์ด๋ฒ„ ๋กœ๊ทธ์ธ

์นด์นด์˜ค ๋กœ๊ทธ์ธ๊ณผ ๋งค์šฐ ์œ ์‚ฌํ•˜๋‹ค.

1. ๋„ค์ด๋ฒ„ ์„œ๋ฒ„-ํ”„๋ก ํŠธ์˜ ์ธ๊ฐ€์ฝ”๋“œ ํ†ต์‹ 

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ํ”„๋ก ํŠธ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•จ์— ๋”ฐ๋ผ ๋„ค์ด๋ฒ„ ์„œ๋ฒ„์—์„œ ๋Œ๋ ค ๋ฐ›์€ ์ธ๊ฐ€์ฝ”๋“œ์™€ state๋ผ๋Š” ํ”„๋กœ์ ํŠธ์—์„œ ์ง€์ •ํ•œ String ๊ฐ’์„ ํ•จ๊ป˜ ๋ฐฑ์—”๋“œ๋กœ ๋„˜๊ฒจ์ฃผ๋ฉด ๋œ๋‹ค.

2. ์นด์นด์˜ค ์„œ๋ฒ„-๋ฐฑ์˜ ์œ ์ € ์ •๋ณด ํ†ต์‹ 

๋‘ ๋ฒˆ์˜ ์š”์ฒญ ์ค‘ ์ฒซ ์š”์ฒญ์˜ ์ฃผ์†Œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์œผ๋ฉฐ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ POST ์š”์ฒญํ•œ๋‹ค.

"https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id="
                + NAVER_KEY + "&client_secret=" + NAVER_SECRET + "&redirect_uri=" + NAVER_URI
                + "&code=" + naverRequest.getCode() + "&state=" + naverRequest.getState()

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋„ค์ด๋ฒ„ ์„œ๋ฒ„์˜ Access Token๊ณผ Refresh Token์„ ๋Œ๋ ค ๋ฐ›๊ณ , ์ด๋ฅผ ํ”„๋ก ํŠธ์— ์ „๋‹ฌํ•˜์ง€ ์•Š๊ณ  ๋‘ ๋ฒˆ์งธ ์š”์ฒญ์— ์‚ฌ์šฉํ•œ๋‹ค.

"https://openapi.naver.com/v1/nid/me"

์œ„ ์ฃผ์†Œ์— ๋‘ ๋ฒˆ์งธ POST ์š”์ฒญ์„ ๋‚ ๋ ค ์•„๋ž˜์™€ ๊ฐ™์€ ํ˜•ํƒœ์˜ ์œ ์ € ์ •๋ณด๋ฅผ ๋ฐ›๋Š”๋‹ค.

์ด ๋•Œ, ์นด์นด์˜ค๋กœ๋ถ€ํ„ฐ ์•„๋ž˜์™€ ๊ฐ™์€ ํ˜•ํƒœ์˜ ์œ ์ € ์ •๋ณด๋ฅผ ๋ฐ›๊ฒŒ ๋œ๋‹ค.

{
    "resultCode": "",
    "message": "",
    "response": {
          "authenticationCode": "",
          "nickname": "",
          "imageUrl": ""
    }
}

์œ„์˜ ๋ฆฌ์Šคํฐ์Šค์—์„œ response์˜ authenticationCode๊ฐ€ uniqueํ•˜๋ฏ€๋กœ ์œ ์ €๋ฅผ ์‹๋ณ„ํ•˜๋Š” ์šฉ๋„๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.


์• ํ”Œ ๋กœ๊ทธ์ธ

์• ํ”Œ ๋กœ๊ทธ์ธ์˜ ๊ฒฝ์šฐ ์œ„์˜ ๋‘ ๋ฐฉ์‹๊ณผ ์กฐ๊ธˆ ๋” ๊ธธ๊ณ  ๋ณต์žกํ•˜๋‹ค.

1. ์• ํ”Œ ์„œ๋ฒ„-๋ฐฑ์˜ ์œ ์ € ์ •๋ณด ํ†ต์‹ 

๋กœ๊ทธ์ธ ์‹œ ๋ฐฑ์—”๋“œ์˜ Post API๋กœ ๋ฐ”๋กœ redirect ์‹œํ‚จ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด MultiValueMap์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.

  	@ApiOperation(value = "์• ํ”Œ ๋กœ๊ทธ์ธ")
    @PostMapping(value = "/login", produces = "application/json; charset=utf-8", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    @ResponseBody
    public void postAppleLogin(@RequestBody MultiValueMap<String, String> formData, HttpServletResponse response){
        ...
    }

์„ค์ •์— ๋”ฐ๋ผ ๋Œ๋ ค ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ์ •๋ณด๊ฐ€ ์ƒ์ดํ•œ๋ฐ, ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฐ›๊ฒŒ ๋˜๋Š” ์ •๋ณด๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. ์ด์ค‘ id_token์œผ๋กœ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ํ•ด์•ผํ•œ๋‹ค.

{
	"state": "",
    "code": "",
    "id_token": "",
    "user": "{}"
}

2. IdentityToken ์œ ํšจ์„ฑ ๊ฒ€์ฆ

"https://appleid.apple.com/auth/keys"

์œ„ ์ฃผ์†Œ์— GET ์š”์ฒญ์„ ํ†ตํ•ด ๊ณต๊ฐœ ํ‚ค๋ฅผ ๋ฐ›์•„ ์˜จ๋‹ค. ๊ฐœ๋ฐœํ•˜๋˜ ๋‹น์‹œ์—๋Š” ์„ธ ๊ฐœ์˜ ํ‚ค๋ฅผ ์ œ๊ณตํ•˜์˜€๋Š”๋ฐ, ๊ทธ ์ค‘ ์œ„์—์„œ ๋ฐ›์€ id_token๊ณผ HEADER์˜ kid์™€ alg ์˜์—ญ์˜ ๊ฐ’์ด ์ผ์น˜ํ•˜๋Š” ํ‚ค๋ฅผ ์ฐพ๋Š”๋‹ค. ํ•ด๋‹น ๊ธฐ๋Šฅ์˜ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

    public AppleSubjectDTO verifyIdentityToken(AppleRequest appleRequest,
        AppleKeyListDTO appleKeyListDTO) {
        PublicKey publicKey = null;
        try {
       		// String์„ JWT ํ˜•ํƒœ๋กœ ๋งคํ•‘ํ•˜๊ณ  ์œ ํšจ ์‹œ๊ฐ„์„ ๊ฒ€์ฆํ•œ๋‹ค.
            SignedJWT signedJWT = SignedJWT.parse(appleRequest.getIdToken());
            JWTClaimsSet payload = signedJWT.getJWTClaimsSet();

            Date currentTime = new Date(System.currentTimeMillis());
            if (!currentTime.before(payload.getExpirationTime())) {
                throw new BadRequestException(ErrorCode.APPLE_TOKEN_EXPIRED.getErrorCode());
            }

			// . ๊ธฐ์ค€์œผ๋กœ IdToken String์˜ HEADER ๋ถ€๋ถ„๋งŒ ๊ฐ€์ ธ์˜จ๋‹ค.
            String headerOfIdentityToken = appleRequest.getIdToken()
                .substring(0, appleRequest.getIdToken().indexOf("."));

			// ๋ณตํ˜ธํ™” ํ•œ ๋’ค kid์™€ alg๋กœ ์ผ์น˜ํ•˜๋Š” key๋ฅผ ์ฐพ๋Š”๋‹ค.
            AppleHeaderDTO appleHeaderDTO = new ObjectMapper().readValue(
                new String(Base64.getDecoder().decode(headerOfIdentityToken), "UTF-8"),
                AppleHeaderDTO.class);
            AppleKeyDTO appleKeyDTO = appleKeyListDTO.getMatchedKeyBy(
                    appleHeaderDTO.getKid(),
                    appleHeaderDTO.getAlg())
                .orElseThrow(
                    () -> new BadRequestException(ErrorCode.APPLE_KEY_UNAVAILABLE.getErrorCode()));
			
            // ๊ตฌํ•œ ๊ฐ’์œผ๋กœ ์ƒˆ๋กœ์šด RSA ๊ณต๊ฐœ ํ‚ค๋ฅผ ๋งŒ๋“ ๋‹ค.
            byte[] nBytes = Base64.getUrlDecoder().decode(appleKeyDTO.getN());
            byte[] eBytes = Base64.getUrlDecoder().decode(appleKeyDTO.getE());

            BigInteger n = new BigInteger(1, nBytes);
            BigInteger e = new BigInteger(1, eBytes);

            RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e);
            KeyFactory keyFactory = KeyFactory.getInstance(appleKeyDTO.getKty());
            publicKey = keyFactory.generatePublic(publicKeySpec);

        } catch (ParseException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException ex) {
            ex.printStackTrace();
        } catch (InvalidKeySpecException ex) {
            ex.printStackTrace();
        } catch (JsonMappingException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        
        // ๋‹ค์‹œ ๋ณตํ˜ธํ™”ํ•˜์—ฌ ์–ป์€ Subject๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
        Claims claims = Jwts.parser().setSigningKey(publicKey)
            .parseClaimsJws(appleRequest.getIdToken())
            .getBody();
        if (!claims.get("nonce").equals(APPLE_NONCE)) {
            throw new BadRequestException(ErrorCode.APPLE_NONCE_INCORRECT.getErrorCode());
        }
        return new AppleSubjectDTO(claims.getSubject());
    }

๋งˆ์ง€๋ง‰์— ์–ป์€ Subject๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ AppleSubjectDTO์— ๊ฐ์‹ธ์ฃผ์—ˆ์œผ๋ฉฐ, ์ด String ๊ฐ’์ด ์œ ์ € ์‹๋ณ„ ์šฉ๋„๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” authenticationCode์— ํ•ด๋‹นํ•œ๋‹ค.

public class AppleSubjectDTO {

    private String authenticationCode;

}

์—ฌ๊ธฐ๊นŒ์ง€๋งŒ ์ง„ํ–‰ํ•ด๋„ ๋กœ๊ทธ์ธ ์ž์ฒด๋Š” ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์•ฑ ์ถœ์‹œ๋ฅผ ์œ„ํ•ด์„œ๋Š” ์ผ๋ จ์˜ ๊ณผ์ •์ด ๋” ํ•„์š”ํ•˜๋‹ค. 2022๋…„๋ถ€ํ„ฐ ์•ฑ์Šคํ† ์–ด ์ถœ์‹œ๋ฅผ ์œ„ํ•ด์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์„œ๋น„์Šค ํƒˆํ‡ดํ•  ๋•Œ DB์—์„œ๋งŒ ์‚ญ์ œํ•  ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์• ํ”Œ์—๋„ ํ•ด๋‹น ๊ณ„์ •์ด ํƒˆํ‡ดํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค.

"https://appleid.apple.com/auth/token?client_id=" + APPLE_CLIENT_ID + "&client_secret="
                + APPLE_SECRET + "&grant_type=authorization_code&code=" + appleRequest.getCode()
                + "&redirect_uri=" + APPLE_URI + option

Client Secret์„ ๋งŒ๋“ค๊ณ  ์œ„์˜ ์ฃผ์†Œ์— POST ์š”์ฒญ์„ ๋‚ ๋ฆฌ๋ฉด ์• ํ”Œ ๊ณ ์œ ์˜ Access Token๊ณผ Refresh Token์„ ์ œ๊ณตํ•œ๋‹ค. ํ•ด๋‹น ๊ณผ์ •์ด ๋ณต์žกํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ๊ธ€์—์„œ ํƒˆํ‡ด์— ๋Œ€ํ•ด ๋”ฐ๋กœ ๋‹ค๋ฃจ์–ด ๋ณด๋ ค๊ณ  ํ•œ๋‹ค. ์•„๋ฌด์ชผ๋ก ์ด ๋•Œ ์–ป์€ Refresh Token๊ณผ Client Secret์„ ํ™œ์šฉํ•ด "https://appleid.apple.com/auth/revoke"์— POST ์š”์ฒญํ•˜์—ฌ ํƒˆํ‡ดํ•  ์ˆ˜ ์žˆ๋‹ค.


OAuth 2.0 ๋กœ๊ทธ์ธ ํ›„ ์ž์ฒด ์„œ๋น„์Šค JWT Token์„ ํ™œ์šฉํ•œ ์ž๋™ ๋กœ๊ทธ์ธ ๊ตฌํ˜„์€ ๋‹ค์Œ ํฌ์ŠคํŠธ์— ์ž‘์„ฑํ•˜๋ ค๊ณ  ํ•œ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€