...

Source file src/github.com/lestrrat-go/jwx/jwt/openid/openid_test.go

Documentation: github.com/lestrrat-go/jwx/jwt/openid

     1  package openid_test
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strconv"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/lestrrat-go/jwx/internal/json"
    11  	"github.com/lestrrat-go/jwx/internal/jwxtest"
    12  
    13  	"github.com/lestrrat-go/jwx/jwa"
    14  	"github.com/lestrrat-go/jwx/jwt"
    15  	"github.com/lestrrat-go/jwx/jwt/internal/types"
    16  	"github.com/lestrrat-go/jwx/jwt/openid"
    17  	"github.com/stretchr/testify/assert"
    18  )
    19  
    20  const aLongLongTimeAgo = 233431200
    21  const aLongLongTimeAgoString = "233431200"
    22  const (
    23  	tokenTime = 233431200
    24  )
    25  
    26  var expectedTokenTime = time.Unix(tokenTime, 0).UTC()
    27  
    28  func testStockAddressClaim(t *testing.T, x *openid.AddressClaim) {
    29  	t.Helper()
    30  	if !assert.NotNil(t, x) {
    31  		return
    32  	}
    33  
    34  	tests := []struct {
    35  		Accessor func() string
    36  		KeyName  string
    37  		Value    string
    38  	}{
    39  		{
    40  			Accessor: x.Formatted,
    41  			KeyName:  openid.AddressFormattedKey,
    42  			Value:    "〒105-0011 東京都港区芝公園4丁目2−8",
    43  		},
    44  		{
    45  			Accessor: x.Country,
    46  			KeyName:  openid.AddressCountryKey,
    47  			Value:    "日本",
    48  		},
    49  		{
    50  			Accessor: x.Region,
    51  			KeyName:  openid.AddressRegionKey,
    52  			Value:    "東京都",
    53  		},
    54  		{
    55  			Accessor: x.Locality,
    56  			KeyName:  openid.AddressLocalityKey,
    57  			Value:    "港区",
    58  		},
    59  		{
    60  			Accessor: x.StreetAddress,
    61  			KeyName:  openid.AddressStreetAddressKey,
    62  			Value:    "芝公園4丁目2−8",
    63  		},
    64  		{
    65  			Accessor: x.PostalCode,
    66  			KeyName:  openid.AddressPostalCodeKey,
    67  			Value:    "105-0011",
    68  		},
    69  	}
    70  
    71  	for _, tc := range tests {
    72  		tc := tc
    73  		t.Run(tc.KeyName, func(t *testing.T) {
    74  			t.Run("Accessor", func(t *testing.T) {
    75  				if !assert.Equal(t, tc.Value, tc.Accessor(), "values should match") {
    76  					return
    77  				}
    78  			})
    79  			t.Run("Get", func(t *testing.T) {
    80  				v, ok := x.Get(tc.KeyName)
    81  				if !assert.True(t, ok, `x.Get should succeed`) {
    82  					return
    83  				}
    84  				if !assert.Equal(t, tc.Value, v, `values should match`) {
    85  					return
    86  				}
    87  			})
    88  		})
    89  	}
    90  }
    91  
    92  func TestAdressClaim(t *testing.T) {
    93  	const src = `{
    94      "formatted": "〒105-0011 東京都港区芝公園4丁目2−8",
    95  		"street_address": "芝公園4丁目2−8",
    96  		"locality": "港区",
    97  		"region": "東京都",
    98  		"postal_code": "105-0011",
    99  		"country": "日本"
   100  	}`
   101  
   102  	var address openid.AddressClaim
   103  	if !assert.NoError(t, json.Unmarshal([]byte(src), &address), "json.Unmarshal should succeed") {
   104  		return
   105  	}
   106  
   107  	var roundtrip openid.AddressClaim
   108  	buf, err := json.Marshal(address)
   109  	if !assert.NoError(t, err, `json.Marshal(address) should succeed`) {
   110  		return
   111  	}
   112  
   113  	if !assert.NoError(t, json.Unmarshal(buf, &roundtrip), "json.Unmarshal should succeed") {
   114  		return
   115  	}
   116  
   117  	for _, x := range []*openid.AddressClaim{&address, &roundtrip} {
   118  		testStockAddressClaim(t, x)
   119  	}
   120  }
   121  
   122  func TestOpenIDClaims(t *testing.T) {
   123  	getVerify := func(token openid.Token, key string, expected interface{}) bool {
   124  		v, ok := token.Get(key)
   125  		if !assert.True(t, ok, `token.Get %#v should succeed`, key) {
   126  			return false
   127  		}
   128  		return assert.Equal(t, v, expected)
   129  	}
   130  
   131  	var base = []struct {
   132  		Value    interface{}
   133  		Expected func(interface{}) interface{}
   134  		Check    func(openid.Token)
   135  		Key      string
   136  	}{
   137  		{
   138  			Key:   openid.AudienceKey,
   139  			Value: []string{"developers", "secops", "tac"},
   140  			Check: func(token openid.Token) {
   141  				assert.Equal(t, token.Audience(), []string{"developers", "secops", "tac"})
   142  			},
   143  		},
   144  		{
   145  			Key:   openid.ExpirationKey,
   146  			Value: tokenTime,
   147  			Expected: func(v interface{}) interface{} {
   148  				var n types.NumericDate
   149  				if err := n.Accept(v); err != nil {
   150  					panic(err)
   151  				}
   152  				return n.Get()
   153  			},
   154  			Check: func(token openid.Token) {
   155  				assert.Equal(t, token.Expiration(), expectedTokenTime)
   156  			},
   157  		},
   158  		{
   159  			Key:   openid.IssuedAtKey,
   160  			Value: tokenTime,
   161  			Expected: func(v interface{}) interface{} {
   162  				var n types.NumericDate
   163  				if err := n.Accept(v); err != nil {
   164  					panic(err)
   165  				}
   166  				return n.Get()
   167  			},
   168  			Check: func(token openid.Token) {
   169  				assert.Equal(t, token.Expiration(), expectedTokenTime)
   170  			},
   171  		},
   172  		{
   173  			Key:   openid.IssuerKey,
   174  			Value: "http://www.example.com",
   175  			Check: func(token openid.Token) {
   176  				assert.Equal(t, token.Issuer(), "http://www.example.com")
   177  			},
   178  		},
   179  		{
   180  			Key:   openid.JwtIDKey,
   181  			Value: "e9bc097a-ce51-4036-9562-d2ade882db0d",
   182  			Check: func(token openid.Token) {
   183  				assert.Equal(t, token.JwtID(), "e9bc097a-ce51-4036-9562-d2ade882db0d")
   184  			},
   185  		},
   186  		{
   187  			Key:   openid.NotBeforeKey,
   188  			Value: tokenTime,
   189  			Expected: func(v interface{}) interface{} {
   190  				var n types.NumericDate
   191  				if err := n.Accept(v); err != nil {
   192  					panic(err)
   193  				}
   194  				return n.Get()
   195  			},
   196  			Check: func(token openid.Token) {
   197  				assert.Equal(t, token.NotBefore(), expectedTokenTime)
   198  			},
   199  		},
   200  		{
   201  			Key:   openid.SubjectKey,
   202  			Value: "unit test",
   203  			Check: func(token openid.Token) {
   204  				assert.Equal(t, token.Subject(), "unit test")
   205  			},
   206  		},
   207  		{
   208  			Value: "jwx",
   209  			Key:   openid.NameKey,
   210  			Check: func(token openid.Token) {
   211  				assert.Equal(t, token.Name(), "jwx")
   212  			},
   213  		},
   214  		{
   215  			Value: "jay",
   216  			Key:   openid.GivenNameKey,
   217  			Check: func(token openid.Token) {
   218  				assert.Equal(t, token.GivenName(), "jay")
   219  			},
   220  		},
   221  		{
   222  			Value: "weee",
   223  			Key:   openid.MiddleNameKey,
   224  			Check: func(token openid.Token) {
   225  				assert.Equal(t, token.MiddleName(), "weee")
   226  			},
   227  		},
   228  		{
   229  			Value: "xi",
   230  			Key:   openid.FamilyNameKey,
   231  			Check: func(token openid.Token) {
   232  				assert.Equal(t, token.FamilyName(), "xi")
   233  			},
   234  		},
   235  		{
   236  			Value: "jayweexi",
   237  			Key:   openid.NicknameKey,
   238  			Check: func(token openid.Token) {
   239  				assert.Equal(t, token.Nickname(), "jayweexi")
   240  			},
   241  		},
   242  		{
   243  			Value: "jwx",
   244  			Key:   openid.PreferredUsernameKey,
   245  			Check: func(token openid.Token) {
   246  				assert.Equal(t, token.PreferredUsername(), "jwx")
   247  			},
   248  		},
   249  		{
   250  			Value: "https://github.com/lestrrat-go/jwx",
   251  			Key:   openid.ProfileKey,
   252  			Check: func(token openid.Token) {
   253  				assert.Equal(t, token.Profile(), "https://github.com/lestrrat-go/jwx")
   254  			},
   255  		},
   256  		{
   257  			Value: "https://avatars1.githubusercontent.com/u/36653903?s=400&v=4",
   258  			Key:   openid.PictureKey,
   259  			Check: func(token openid.Token) {
   260  				assert.Equal(t, token.Picture(), "https://avatars1.githubusercontent.com/u/36653903?s=400&v=4")
   261  			},
   262  		},
   263  		{
   264  			Value: "https://github.com/lestrrat-go/jwx",
   265  			Key:   openid.WebsiteKey,
   266  			Check: func(token openid.Token) {
   267  				assert.Equal(t, token.Website(), "https://github.com/lestrrat-go/jwx")
   268  			},
   269  		},
   270  		{
   271  			Value: "lestrrat+github@gmail.com",
   272  			Key:   openid.EmailKey,
   273  			Check: func(token openid.Token) {
   274  				assert.Equal(t, token.Email(), "lestrrat+github@gmail.com")
   275  			},
   276  		},
   277  		{
   278  			Value: true,
   279  			Key:   openid.EmailVerifiedKey,
   280  			Check: func(token openid.Token) {
   281  				assert.True(t, token.EmailVerified())
   282  			},
   283  		},
   284  		{
   285  			Value: "n/a",
   286  			Key:   openid.GenderKey,
   287  			Check: func(token openid.Token) {
   288  				assert.Equal(t, token.Gender(), "n/a")
   289  			},
   290  		},
   291  		{
   292  			Value: "2015-11-04",
   293  			Key:   openid.BirthdateKey,
   294  			Expected: func(v interface{}) interface{} {
   295  				var b openid.BirthdateClaim
   296  				if err := b.Accept(v); err != nil {
   297  					panic(err)
   298  				}
   299  				return &b
   300  			},
   301  			Check: func(token openid.Token) {
   302  				var b openid.BirthdateClaim
   303  				b.Accept("2015-11-04")
   304  				assert.Equal(t, token.Birthdate(), &b)
   305  			},
   306  		},
   307  		{
   308  			Value: "Asia/Tokyo",
   309  			Key:   openid.ZoneinfoKey,
   310  			Check: func(token openid.Token) {
   311  				assert.Equal(t, token.Zoneinfo(), "Asia/Tokyo")
   312  			},
   313  		},
   314  		{
   315  			Value: "ja_JP",
   316  			Key:   openid.LocaleKey,
   317  			Check: func(token openid.Token) {
   318  				assert.Equal(t, token.Locale(), "ja_JP")
   319  			},
   320  		},
   321  		{
   322  			Value: "819012345678",
   323  			Key:   openid.PhoneNumberKey,
   324  			Check: func(token openid.Token) {
   325  				assert.Equal(t, token.PhoneNumber(), "819012345678")
   326  			},
   327  		},
   328  		{
   329  			Value: true,
   330  			Key:   openid.PhoneNumberVerifiedKey,
   331  			Check: func(token openid.Token) {
   332  				assert.True(t, token.PhoneNumberVerified())
   333  			},
   334  		},
   335  		{
   336  			Value: map[string]interface{}{
   337  				"formatted":      "〒105-0011 東京都港区芝公園4丁目2−8",
   338  				"street_address": "芝公園4丁目2−8",
   339  				"locality":       "港区",
   340  				"region":         "東京都",
   341  				"country":        "日本",
   342  				"postal_code":    "105-0011",
   343  			},
   344  			Key: openid.AddressKey,
   345  			Expected: func(v interface{}) interface{} {
   346  				address := openid.NewAddress()
   347  				m, ok := v.(map[string]interface{})
   348  				if !ok {
   349  					panic(fmt.Sprintf("expected map[string]interface{}, got %T", v))
   350  				}
   351  				for name, val := range m {
   352  					if !assert.NoError(t, address.Set(name, val), `address.Set should succeed`) {
   353  						return nil
   354  					}
   355  				}
   356  				return address
   357  			},
   358  			Check: func(token openid.Token) {
   359  				testStockAddressClaim(t, token.Address())
   360  			},
   361  		},
   362  		{
   363  			Value: aLongLongTimeAgoString,
   364  			Key:   openid.UpdatedAtKey,
   365  			Expected: func(v interface{}) interface{} {
   366  				var n types.NumericDate
   367  				if err := n.Accept(v); err != nil {
   368  					panic(err)
   369  				}
   370  				return n.Get()
   371  			},
   372  			Check: func(token openid.Token) {
   373  				assert.Equal(t, time.Unix(aLongLongTimeAgo, 0).UTC(), token.UpdatedAt())
   374  			},
   375  		},
   376  		{
   377  			Value: `dummy`,
   378  			Key:   `dummy`,
   379  			Check: func(token openid.Token) {
   380  				v, ok := token.Get(`dummy`)
   381  				if !assert.True(t, ok, `token.Get should return valid value`) {
   382  					return
   383  				}
   384  				if !assert.Equal(t, `dummy`, v, `values should match`) {
   385  					return
   386  				}
   387  			},
   388  		},
   389  	}
   390  
   391  	var data = map[string]interface{}{}
   392  	var expected = map[string]interface{}{}
   393  	for _, value := range base {
   394  		data[value.Key] = value.Value
   395  		if expf := value.Expected; expf != nil {
   396  			expected[value.Key] = expf(value.Value)
   397  		} else {
   398  			expected[value.Key] = value.Value
   399  		}
   400  	}
   401  
   402  	type openidTokTestCase struct {
   403  		Token openid.Token
   404  		Name  string
   405  	}
   406  	var tokens []openidTokTestCase
   407  
   408  	{ // one with Set()
   409  		token := openid.New()
   410  		for name, value := range data {
   411  			if !assert.NoError(t, token.Set(name, value), `token.Set should succeed`) {
   412  				return
   413  			}
   414  		}
   415  		tokens = append(tokens, openidTokTestCase{Name: `token constructed by calling Set()`, Token: token})
   416  	}
   417  
   418  	{ // two with json.Marshal / json.Unmarshal
   419  		src, err := json.MarshalIndent(data, "", "  ")
   420  		if !assert.NoError(t, err, `failed to marshal base map`) {
   421  			return
   422  		}
   423  
   424  		t.Logf("Using source JSON: %s", src)
   425  
   426  		token := openid.New()
   427  		if !assert.NoError(t, json.Unmarshal(src, &token), `json.Unmarshal should succeed`) {
   428  			return
   429  		}
   430  		tokens = append(tokens, openidTokTestCase{Name: `token constructed by Marshal(map)+Unmashal`, Token: token})
   431  
   432  		// One more... Marshal the token, _and_ re-unmarshal
   433  		buf, err := json.Marshal(token)
   434  		if !assert.NoError(t, err, `json.Marshal should succeed`) {
   435  			return
   436  		}
   437  
   438  		token2 := openid.New()
   439  		if !assert.NoError(t, json.Unmarshal(buf, &token2), `json.Unmarshal should succeed`) {
   440  			return
   441  		}
   442  		tokens = append(tokens, openidTokTestCase{Name: `token constructed by Marshal(openid.Token)+Unmashal`, Token: token2})
   443  
   444  		// Sign it, and use jwt.Parse
   445  
   446  		var token3 openid.Token
   447  		{
   448  			alg := jwa.RS256
   449  			key, err := jwxtest.GenerateRsaKey()
   450  			if !assert.NoError(t, err, `rsa.GeneraKey should succeed`) {
   451  				return
   452  			}
   453  			signed, err := jwt.Sign(token, alg, key)
   454  			if !assert.NoError(t, err, `jwt.Sign should succeed`) {
   455  				return
   456  			}
   457  
   458  			tokenTmp, err := jwt.Parse(signed, jwt.WithToken(openid.New()), jwt.WithVerify(alg, &key.PublicKey))
   459  			if !assert.NoError(t, err, `parsing the token via jwt.Parse should succeed`) {
   460  				return
   461  			}
   462  
   463  			// Check if token is an OpenID token
   464  			if _, ok := tokenTmp.(openid.Token); !assert.True(t, ok, `token should be a openid.Token (%T)`, tokenTmp) {
   465  				return
   466  			}
   467  			token3 = tokenTmp.(openid.Token)
   468  		}
   469  
   470  		tokens = append(tokens, openidTokTestCase{Name: `token constructed by jwt.Parse`, Token: token3})
   471  	}
   472  
   473  	for _, token := range tokens {
   474  		token := token
   475  		t.Run(token.Name, func(t *testing.T) {
   476  			for _, value := range base {
   477  				value := value
   478  				t.Run(value.Key, func(t *testing.T) {
   479  					value.Check(token.Token)
   480  				})
   481  				t.Run(value.Key+" via Get()", func(t *testing.T) {
   482  					expected := value.Value
   483  					if expf := value.Expected; expf != nil {
   484  						expected = expf(value.Value)
   485  					}
   486  					getVerify(token.Token, value.Key, expected)
   487  				})
   488  			}
   489  		})
   490  	}
   491  
   492  	t.Run("Iterator", func(t *testing.T) {
   493  		v := tokens[0].Token
   494  		t.Run("Iterate", func(t *testing.T) {
   495  			seen := make(map[string]interface{})
   496  			for iter := v.Iterate(context.TODO()); iter.Next(context.TODO()); {
   497  				pair := iter.Pair()
   498  				seen[pair.Key.(string)] = pair.Value
   499  
   500  				getV, ok := v.Get(pair.Key.(string))
   501  				if !assert.True(t, ok, `v.Get should succeed for key %#v`, pair.Key) {
   502  					return
   503  				}
   504  				if !assert.Equal(t, pair.Value, getV, `pair.Value should match value from v.Get()`) {
   505  					return
   506  				}
   507  			}
   508  			if !assert.Equal(t, expected, seen, `values should match`) {
   509  				return
   510  			}
   511  		})
   512  		t.Run("Walk", func(t *testing.T) {
   513  			seen := make(map[string]interface{})
   514  			v.Walk(context.TODO(), openid.VisitorFunc(func(key string, value interface{}) error {
   515  				seen[key] = value
   516  				return nil
   517  			}))
   518  			if !assert.Equal(t, expected, seen, `values should match`) {
   519  				return
   520  			}
   521  		})
   522  		t.Run("AsMap", func(t *testing.T) {
   523  			seen, err := v.AsMap(context.TODO())
   524  			if !assert.NoError(t, err, `v.AsMap should succeed`) {
   525  				return
   526  			}
   527  			if !assert.Equal(t, expected, seen, `values should match`) {
   528  				return
   529  			}
   530  		})
   531  		t.Run("Clone", func(t *testing.T) {
   532  			cloned, err := v.Clone()
   533  			if !assert.NoError(t, err, `v.Clone should succeed`) {
   534  				return
   535  			}
   536  
   537  			if !assert.True(t, jwt.Equal(v, cloned), `values should match`) {
   538  				return
   539  			}
   540  		})
   541  	})
   542  }
   543  
   544  func TestBirthdateClaim(t *testing.T) {
   545  	t.Parallel()
   546  	t.Run("regular date", func(t *testing.T) {
   547  		t.Parallel()
   548  		testcases := []struct {
   549  			Source string
   550  			Year   int
   551  			Month  int
   552  			Day    int
   553  			Error  bool
   554  		}{
   555  			{
   556  				Source: `"2015-11-04"`,
   557  				Year:   2015,
   558  				Month:  11,
   559  				Day:    4,
   560  			},
   561  			{
   562  				Source: `"0009-09-09"`,
   563  				Year:   9,
   564  				Month:  9,
   565  				Day:    9,
   566  			},
   567  			{
   568  				Source: `{}`,
   569  				Error:  true,
   570  			},
   571  			{
   572  				Source: `"202X-01-01"`,
   573  				Error:  true,
   574  			},
   575  			{
   576  				Source: `"0000-01-01"`,
   577  				Error:  true,
   578  			},
   579  			{
   580  				Source: `"0001-00-01"`,
   581  				Error:  true,
   582  			},
   583  			{
   584  				Source: `"0001-01-00"`,
   585  				Error:  true,
   586  			},
   587  		}
   588  
   589  		for _, tc := range testcases {
   590  			tc := tc
   591  			t.Run(tc.Source, func(t *testing.T) {
   592  				var b openid.BirthdateClaim
   593  				if tc.Error {
   594  					assert.Error(t, json.Unmarshal([]byte(tc.Source), &b), `json.Unmarshal should fail`)
   595  					return
   596  				}
   597  
   598  				if !assert.NoError(t, json.Unmarshal([]byte(tc.Source), &b), `json.Unmarshal should succeed`) {
   599  					return
   600  				}
   601  
   602  				if !assert.Equal(t, b.Year(), tc.Year, "year should match") {
   603  					return
   604  				}
   605  				if !assert.Equal(t, b.Month(), tc.Month, "month should match") {
   606  					return
   607  				}
   608  				if !assert.Equal(t, b.Day(), tc.Day, "day should match") {
   609  					return
   610  				}
   611  				serialized, err := json.Marshal(b)
   612  				if !assert.NoError(t, err, `json.Marshal should succeed`) {
   613  					return
   614  				}
   615  				if !assert.Equal(t, string(serialized), tc.Source, `serialized format should be the same`) {
   616  					return
   617  				}
   618  				stringified := b.String()
   619  				expectedString, _ := strconv.Unquote(tc.Source)
   620  				if !assert.Equal(t, stringified, expectedString, `stringified format should be the same`) {
   621  					return
   622  				}
   623  			})
   624  		}
   625  	})
   626  	t.Run("empty date", func(t *testing.T) {
   627  		t.Parallel()
   628  		var b openid.BirthdateClaim
   629  		if !assert.Equal(t, b.Year(), 0, "year should match") {
   630  			return
   631  		}
   632  		if !assert.Equal(t, b.Month(), 0, "month should match") {
   633  			return
   634  		}
   635  		if !assert.Equal(t, b.Day(), 0, "day should match") {
   636  			return
   637  		}
   638  	})
   639  	t.Run("invalid accept", func(t *testing.T) {
   640  		t.Parallel()
   641  		var b openid.BirthdateClaim
   642  		if !assert.Error(t, b.Accept(nil)) {
   643  			return
   644  		}
   645  	})
   646  }
   647  
   648  func TestKeys(t *testing.T) {
   649  	at := assert.New(t)
   650  	at.Equal(`address`, openid.AddressKey)
   651  	at.Equal(`aud`, openid.AudienceKey)
   652  	at.Equal(`birthdate`, openid.BirthdateKey)
   653  	at.Equal(`email`, openid.EmailKey)
   654  	at.Equal(`email_verified`, openid.EmailVerifiedKey)
   655  	at.Equal(`exp`, openid.ExpirationKey)
   656  	at.Equal(`family_name`, openid.FamilyNameKey)
   657  	at.Equal(`gender`, openid.GenderKey)
   658  	at.Equal(`given_name`, openid.GivenNameKey)
   659  	at.Equal(`iat`, openid.IssuedAtKey)
   660  	at.Equal(`iss`, openid.IssuerKey)
   661  	at.Equal(`jti`, openid.JwtIDKey)
   662  	at.Equal(`locale`, openid.LocaleKey)
   663  	at.Equal(`middle_name`, openid.MiddleNameKey)
   664  	at.Equal(`name`, openid.NameKey)
   665  	at.Equal(`nickname`, openid.NicknameKey)
   666  	at.Equal(`nbf`, openid.NotBeforeKey)
   667  	at.Equal(`phone_number`, openid.PhoneNumberKey)
   668  	at.Equal(`phone_number_verified`, openid.PhoneNumberVerifiedKey)
   669  	at.Equal(`picture`, openid.PictureKey)
   670  	at.Equal(`preferred_username`, openid.PreferredUsernameKey)
   671  	at.Equal(`profile`, openid.ProfileKey)
   672  	at.Equal(`sub`, openid.SubjectKey)
   673  	at.Equal(`updated_at`, openid.UpdatedAtKey)
   674  	at.Equal(`website`, openid.WebsiteKey)
   675  	at.Equal(`zoneinfo`, openid.ZoneinfoKey)
   676  }
   677  

View as plain text