본문 바로가기

Golang/etc

Golang Apple Login (애플 로그인)

728x90
반응형

전 포스팅에서 열심히 jwt, jwk 에 대해 설명했으니 이제 코드로 직접 구현하는 법을 작성하겠다. 

 

글쓴이가 아주아주 귀찮은 관계로 서버가 로그인하여 애플 인증서버로 부터

- state

- code

- id_token 

값을 이미 받았다는 전제하에 포스팅을 할 예정이다. 

 

이전 단계는 다른 oauth2.0 로그인 방식과 똑같기 때문에 생략.

 

type authResponse struct {
	State   string `query:"state"`
	Code    string `query:"code"`
	IdToken string `query:"id_token"`
}

reqAuth := authResponse{}
	if err = c.Bind(&reqAuth); err != nil {
		if uri := c.Request().URL; uri != nil {
			a.logger.WithError(err).Errorf("[sns-apple] query string parse failure - %+v", uri.RawQuery)
		} else {
			a.logger.WithError(err).Errorf("[sns-apple] - %+v", c.Request().RequestURI)
		}
		return
	}
	a.logger.Debugf("authorization response : %+v", reqAuth)

	if reqAuth.Code == "" {
		a.logger.Error("response failure ")
		return
	}

c는 resty client의 c 이다. 참고하시라고 씀.

 

이렇게 response로 state, code, id_token을 받아왔으면 이제부터 거쳐야 하는 단계는 아래와 같다. 

 

  1. jwk 받아오기 
  2. 3개의 public key중 id_token의 kid와 일치하는 public key 골라내기
  3. 골라낸 1개의 public key의 내용 중 n, e 값을 사용하여 signature verify할 수 있는 key 생성하기
  4. 3번에서 생성한 key로 verify하기
  5. verify된 payload에 있는 값 저장 (사유 : 사용자 email 정보 등이 담겨있음)
  6. 애플 개발자 계정에서 얻을 수 있는 값들로 claim 생성하기
  7. private key로 애플 인증서버로 보낼 jwt 서명하기 (private key는 apple developer 사이트에서 저장하라고 줌)
  8. 7에서 서명한 jwt를 client_secret이라는 변수에 담아 (다른 정보들도 함께) formData로 보내기
  9. apple에서 refresh token 주는지 확인
  10. 끝. 

 

내가 이해한 방식으로 최대한 자세히 적었다.

나중에 내가 볼때 헷갈리지 말라는 이유도 있고,

나와 같은 초보개발자들이 보고 도움이 조금이라도 되시라고.. 

 

그러면 1번부터 code로 구현해보면 

const ApplePublicKey = "https://appleid.apple.com/auth/keys"

type appleKey struct {
	Kty string `json:"kty"`
	Kid string `json:"kid"`
	Use string `json:"use"`
	Als string `json:"als"`
	N   string `json:"n"`
	E   string `json:"e"`
}

type applePublicKey struct {
	Keys []appleKey `json:"keys"`
}

pubKey := applePublicKey{}
	pubKeyResult, err := a.client.R().SetResult(&pubKey).Get(ApplePublicKey)
	if err != nil {
		a.logger.WithError(err).Error("get apple public key err")
	}

 

jwk를 제공하는 아래 주소를 통해 result를 받아온다. 

const ApplePublicKey = "https://appleid.apple.com/auth/keys"

그러면 pubKey에 3개의 jwk 정보가 담긴다. 

 

2. 3개의 public key중 id_token의 kid와 일치하는 public key 골라내기

idTk, err := jwt.Parse(reqAuth.IdToken, func(token *jwt.Token) (interface{}, error) {
		// kid 값 저장 | public key 대조에 필요하기 때문에
        appleIdToken.Header.Kid = token.Header["kid"].(string)
	
		for _, v := range pubKey.Keys {
        	// 받아온 public key중 id_token과 kid 일치하는지 확인 후 n, e 값 저장
			if appleIdToken.Header.Kid == v.Kid {
				n, _ := base64.RawURLEncoding.DecodeString(v.N)
				publicSecret.N = n
				e, _ := base64.URLEncoding.DecodeString(v.E)
				publicSecret.E = e
				break
			}
		}
		
		publicKeyE := binary.LittleEndian.Uint32(append(publicSecret.E, 0))

		// 이 rsaKey로 id_token verify 
		rsaKey := &rsa.PublicKey{
			N: new(big.Int).SetBytes(n),
			E: int(publicKeyE),
		}
		return rsaKey, nil
	})
	if err != nil {
		a.logger.WithError(err).Error("idTk parse failed.")
	}

쪼개기가 애매한 코드라서 그냥 냅다 쓰고 다시 설명해보면

 

pubKey.Keys로 돌리는 for문에서 kid가 일치하는 public key를 골라내고 있다.

그리고 그 n, e 값을 저장한다. 단 base64로 인코딩 되어 있기 때문에 decoding은 필수다. 

 

n, _ := base64.RawURLEncoding.DecodeString(pubKey.N)
e, _ := base64.StdEncoding.DecodeString(pubKey.E)

golang에서 제공하는 base64 패키지의 경우 여러 방식으로 decoding할 수 있는데..

애플 로그인할때는 무조건 RawURLEncoding을 사용하세요.. 다른 방법으로 하면 디코딩이 제대로 안됩니다.

 

publicKeyE := binary.LittleEndian.Uint32(append(e, 0))

e 값의 경우 65537로 고정되어 있다고는 하는데.. 그냥 맘편하게 decoding 해줬다.

참고로 Unit32로 디코딩해야지 안그러면 에러난다. 수치스러운 panic을 맛보게 된다.. 

 

막 테스트한다고 타입 통일도 못하고 마구잡이로 썼는데

이글을 보시는 여러분은 처음부터 예쁘게 구조를 잡고 하시는것을 추천드립니다

 

3. 여튼 힘들게 얻어내고 디코딩까지 마친 n,e 값으로 public key를 생성해준다. 

rsaKey := &rsa.PublicKey{
			N: new(big.Int).SetBytes(n),
			E: int(publicKeyE),
          }

4. 3에서 생성한 public key로 id_token verify 하기 

return rsaKey, nil

jwt.Parse()함수의 매개변수로 사용한 callback 함수인 keyFunc 의 리턴값으로 rsaKey를 넘기면 완성이다. 

혹시나해서 KeyFunc 설명을 자세히 보니 kid 사용해서 identify할때 쓰란다. 적재적소에 잘 쓴 케이스라고 볼 수 있다. 

 

 

5. 이제 verify가 완료된 idTk의 payload 값을 받아와서 맞는 정보인지 검증한다. 그리고 사용자 email과 같은 값을 서버에 저장하면됨.

claims, ok := idTk.Claims.(jwt.MapClaims)
	if !ok && !idTk.Valid {
		a.logger.WithError(err).Error("token is not valid")
	}
	for k, v := range claims {
		a.logger.Infof("k : %s, v : %+v", k, v)
		switch k {
		case "email":
			appleIDToken.Email = v.(string)
		}
	}

 

 

6. 애플 개발자 계정에서 얻을 수 있는 값들로 claim 생성하기

appleClaims := jwt.MapClaims{
		"iss": {TEAM_ID},
		"aud": "https://appleid.apple.com",
		"exp": time.Now().UTC().Add(24 * time.Hour * 100).Unix(),
		"iat": time.Now().UTC().Unix(),
		"sub": {CLIENT_ID},
	}

코드 상에서는 적지 않았는데 애플쪽에서 제공한 private key로 다시 서명해서 jwt token을 생성해준다.

alg은 ES256을 사용하기 때문에 jwt.SigningMethodES256을 사용한다. 

kid 는 Apple Developer 페이지에 명시되어있는 Key ID 값을 넣어준다. 

 

iss는 Apple Developer 페이지에 명시되어있는 Team ID를 넣어주면 된다.

iat는 토큰이 만들어진 일시이기 때문에 time.Now().UINX()를 해준 값을 대입한다. 단, UTC로 보내야한다. 

exp는 토큰이 만료될 일시이기 때문에 180일만 안넘게 넣으면된다. 나의 경우 그냥 100일로 셋팅했다.

 

앞서 id_token을 verify하는 주체는 우리쪽 서버였지만 이번에는 애플 인증서버에서 verify할것이기 때문에 

aud는"https://appleid.apple.com" 값을 입력한다.

 

sub는client_id 값을 입력한다.

 

 

 

 

 

7. private key로 애플 인증서버로 보낼 jwt 서명하기 (private key는 apple developer 사이트에서 저장하라고 줌)

appleToken := jwt.NewWithClaims(jwt.SigningMethodES256, appleClaims)
	appleToken.Header["kid"] = AppleKeyID
	
	clientSecret, err := appleToken.SignedString(pemEncoded)

이걸 client_secret 변수에 담아서 전송해야 비.로.소 refresh token을 받을 수 있다. 

 

 

8. 7에서 서명한 jwt를 client_secret이라는 변수에 담아 (다른 정보들도 함께) formData로 보내기

formData := map[string]string{
		"client_id":     a.ClientID,
		"client_secret": clientSecret,
		"code":          reqAuth.Code,
		"grant_type":    "authorization_code",
		"redirect_uri":  a.RedirectUrl,
	}

	token := tokenResponse{}
	uri := a.HostURL + "/auth/token"
	result, err := a.client.R().SetFormData(formData).SetResult(&token).Post(uri)
	if err != nil {
		a.logger.WithError(err).Error("response get failure.")
		return tk, err
	}

보내고 나서 tokenResponse (refresh_token 등의 정보) 가 잘 오는지 확인하면

드디어 끝이난다......

 

 

 

728x90
반응형

'Golang > etc' 카테고리의 다른 글

tcp/ip -02,03  (0) 2022.06.27
tcp/ip - 01  (0) 2022.06.22
Apple 로그인 JWT, JWK  (1) 2022.06.14
golang .env 파일 환경변수 셋팅  (0) 2022.04.13
go init()  (0) 2022.04.13