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

:::note info
これは [ZOZO Advent Calendar 2022](https://qiita.com/advent-calendar/2022/zozo) カレンダー Vol.4 の 7 日目の記事です。
:::
以下、Goを使用する前提で話を進めています。

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

# 原因
以下の構造体にパースするつもりでしたが、取得したIDトークンの`email_verified`がstring型だったのでパースできなかったわけですね。
```go
type Profile struct {
    Email         string `json:"email"`
    EmailVerified bool   `json:"email_verified"`
}
```
じゃあEmailVerifiedフィールドをstringにすればいいだけじゃないか、解散解散！
って思われたそこのあなた、公式docを見るとそれだけではダメそうです。


[developerページ](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple#:~:text=Retrieve%20the%20user%E2%80%99s%20information%20from%20Apple%20ID%20servers)には以下のように記載されています。
> 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のどっちかが返ってくるよ`ということですね。

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

なんだこの嫌がらせみたいな仕様！？って思いましたが、[こちらのやりとり](https://community.auth0.com/t/userinfo-email-verified-field-string-boolean-or-both/27553)を見てみるとどうやら仕様ではなくバグっぽいです。

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

# 対応策
`email_verified`がstringでもboolでも受け取れるようにします。
参考までに、AppleのIDプロバイダーから返ってくるIDトークンのペイロードはこんな感じのものを想定しています。
```json
{
  "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をオーバーライドします。

```go
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型に変換する例をよく見ます。
いろいろ用途がありそうなので、頭の片隅に置いておくと役に立つことがあるかもしれません。

ではまた。