6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ZOZOAdvent Calendar 2022

Day 7

AppleログインでIDトークンを取得したら、email_verifiedクレームの型が2種類あって困った話

Last updated at Posted at 2022-12-06

これは ZOZO Advent Calendar 2022 カレンダー Vol.4 の 7 日目の記事です。

以下、Goを使用する前提で話を進めています。

何が起きたのか

Appleログインを実装する過程で、Appleから送られてきたIDトークンを構造体にパースしたら*json.UnmarshalTypeErrorになっちゃいました。

原因

以下の構造体にパースするつもりでしたが、取得したIDトークンのemail_verifiedがstring型だったのでパースできなかったわけですね。

type Profile struct {
    Email         string `json:"email"`
    EmailVerified bool   `json:"email_verified"`
}

じゃあEmailVerifiedフィールドをstringにすればいいだけじゃないか、解散解散!
って思われたそこのあなた、公式docを見るとそれだけではダメそうです。

developerページには以下のように記載されています。

email_verified
A string or Boolean value that indicates whether the service verifies the email. The value can either be a string ("true" or "false") or a Boolean (true or false). The system may not verify email addresses for Sign in with Apple at Work & School users, and this claim is "false" or false for those users.

つまり、email_verifierはサービスがemail検証をするのかどうかを表す値で、型は文字列かboolのどっちかが返ってくるよということですね。

リクエスト時に型を指定するようなパラメータもないので、どちらの型が返ってくるかはデコードするまでのお楽しみ☆くらいに思っておいた方が良さそうです。

なんだこの嫌がらせみたいな仕様!?って思いましたが、こちらのやりとりを見てみるとどうやら仕様ではなくバグっぽいです。

Sign in with Appleが提供されたのが2019/6/3と比較的最近なので、こういった未解決のバグは少なくないのかもしれませんね。

対応策

email_verifiedがstringでもboolでも受け取れるようにします。
参考までに、AppleのIDプロバイダーから返ってくるIDトークンのペイロードはこんな感じのものを想定しています。

{
  "iss": "https://appleid.apple.com",
  "sub": "022311.ofgbdho8y41irquxn5rr7fkytf773lh2.1765",
  "aud": [
    "test_client_id"
  ],
  "exp": 4073714420,
  "iat": 1453272436,
  "nonce": "a0230255d9e5865e7fe4683944833e41f683427f31554824930751dedd9cddf0",
  "email": "test@example.com",
  "email_verified": true,
  "at_hash": "SjjfaAWSdWEvSDfASCmonm",
  "alg": "HS256"
}

json周りの処理を構造体のタグだけで解決できなさそうな時は、MarshalJSON / UnmarshalJSONを使って複雑な処理を実装できます。

今回はApple ID Serverから返ってきたIDトークンを構造体に正しくパースしたいので、json.UnmarshalJSONを追加してjson.Unmarshalをオーバーライドします。

func (p *Profile) UnmarshalJSON(b []byte) error {
    // 新しい構造体を定義して、Profile構造体には直接パースしない
	type profileBool struct {
		Email         string `json:"email"`
		EmailVerified bool   `json:"email_verified"`
	}
	type profileString struct {
		Email         string `json:"email"`
		EmailVerified string `json:"email_verified"`
	}

	var pb profileBool
	err := json.Unmarshal(b, &pb)
    // email_verifiedがstringだった場合、profileBoolではなくprofileStringにマッピングする
	if _, ok := err.(*json.UnmarshalTypeError); ok {
		var ps profileString
		e := json.Unmarshal(b, &ps)
		if e != nil {
			return e
		}
        // stringからboolに変換する
		verifiedBool, e := strconv.ParseBool(ps.EmailVerified)
		if e != nil {
			return e
		}
        // もとのProfileにマッピングする
		p.Email = ps.Email
		p.EmailVerified = verifiedBool

		return nil
	}
	if err != nil {
		return err
	}
	p.Email = pb.Email
	p.EmailVerified = pb.EmailVerified

	return nil
}

これでProfile構造体には必ずbool型のemail_verifiedがパースされるようになりました!🎉

おわりに

なんとも困ったバグですが、このバグのおかげでMarshalJSON / UnmarshalJSONの理解が深まったので良しとします。
MarshalJSON / UnmarshalJSONは他にも、いろんな日時のフォーマットをtime.Time型に変換する例をよく見ます。
いろいろ用途がありそうなので、頭の片隅に置いておくと役に立つことがあるかもしれません。

ではまた。

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?