286 lines
15 KiB
Go
286 lines
15 KiB
Go
|
package oidc
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"crypto/x509"
|
||
|
"encoding/hex"
|
||
|
"encoding/json"
|
||
|
"encoding/pem"
|
||
|
"hash/fnv"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"net/http/httptest"
|
||
|
"net/url"
|
||
|
"path/filepath"
|
||
|
"strings"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"golang.org/x/net/context"
|
||
|
|
||
|
jose "gopkg.in/square/go-jose.v1"
|
||
|
)
|
||
|
|
||
|
func loadKeySet(t *testing.T, keyfiles ...string) jose.JsonWebKeySet {
|
||
|
var set jose.JsonWebKeySet
|
||
|
set.Keys = make([]jose.JsonWebKey, len(keyfiles))
|
||
|
for i, keyfile := range keyfiles {
|
||
|
set.Keys[i], _ = loadKey(t, keyfile)
|
||
|
}
|
||
|
return set
|
||
|
}
|
||
|
|
||
|
func loadKey(t *testing.T, keyfile string) (pub, priv jose.JsonWebKey) {
|
||
|
data, err := ioutil.ReadFile(keyfile)
|
||
|
if err != nil {
|
||
|
t.Fatalf("can't read key file %s: %v", keyfile, err)
|
||
|
}
|
||
|
|
||
|
block, _ := pem.Decode(data)
|
||
|
if block == nil {
|
||
|
t.Fatalf("no PEM data found in key file: %s", keyfile)
|
||
|
}
|
||
|
|
||
|
keyID := hex.EncodeToString(fnv.New64().Sum(block.Bytes))
|
||
|
|
||
|
if strings.HasPrefix(filepath.Base(keyfile), "ecdsa") {
|
||
|
p, err := x509.ParseECPrivateKey(block.Bytes)
|
||
|
if err != nil {
|
||
|
t.Fatalf("failed to parse ecdsa key %s: %v", keyfile, err)
|
||
|
}
|
||
|
priv = jose.JsonWebKey{Key: p, Algorithm: "EC", Use: "sig", KeyID: keyID}
|
||
|
pub = jose.JsonWebKey{Key: p.Public(), Algorithm: "EC", Use: "sig", KeyID: keyID}
|
||
|
} else {
|
||
|
p, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||
|
if err != nil {
|
||
|
t.Fatalf("failed to parse rsa key %s: %v", keyfile, err)
|
||
|
}
|
||
|
priv = jose.JsonWebKey{Key: p, Algorithm: "RSA", Use: "sig", KeyID: keyID}
|
||
|
pub = jose.JsonWebKey{Key: p.Public(), Algorithm: "RSA", Use: "sig", KeyID: keyID}
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func signPayload(t *testing.T, keyfile string, payload []byte) string {
|
||
|
_, key := loadKey(t, keyfile)
|
||
|
var (
|
||
|
signer jose.Signer
|
||
|
err error
|
||
|
)
|
||
|
if strings.HasPrefix(filepath.Base(keyfile), "rsa") {
|
||
|
signer, err = jose.NewSigner(jose.RS512, &key)
|
||
|
} else {
|
||
|
signer, err = jose.NewSigner(jose.ES512, &key)
|
||
|
}
|
||
|
if err != nil {
|
||
|
t.Fatalf("failed to create signer for %s: %v", keyfile, err)
|
||
|
}
|
||
|
jws, err := signer.Sign(payload)
|
||
|
if err != nil {
|
||
|
t.Fatalf("failed to sign payload: %v", err)
|
||
|
}
|
||
|
s, err := jws.CompactSerialize()
|
||
|
if err != nil {
|
||
|
t.Fatalf("failed to serialize signature: %v", err)
|
||
|
}
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
func TestVerify(t *testing.T) {
|
||
|
jwks := loadKeySet(t,
|
||
|
"testdata/ecdsa_521_1.pem",
|
||
|
"testdata/rsa_2048_1.pem",
|
||
|
)
|
||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
if err := json.NewEncoder(w).Encode(jwks); err != nil {
|
||
|
t.Errorf("failed to encode jwks: %v", err)
|
||
|
}
|
||
|
}))
|
||
|
defer s.Close()
|
||
|
|
||
|
ctx, cancel := context.WithCancel(context.Background())
|
||
|
defer cancel()
|
||
|
|
||
|
r := newRemoteKeySet(ctx, s.URL)
|
||
|
|
||
|
tests := []struct {
|
||
|
key string
|
||
|
wantErr bool
|
||
|
}{
|
||
|
{"testdata/ecdsa_521_1.pem", false},
|
||
|
{"testdata/rsa_2048_1.pem", false},
|
||
|
{"testdata/ecdsa_521_2.pem", true},
|
||
|
{"testdata/rsa_2048_2.pem", true},
|
||
|
{"testdata/ecdsa_521_1.pem", false},
|
||
|
{"testdata/rsa_2048_1.pem", false},
|
||
|
}
|
||
|
|
||
|
for _, tc := range tests {
|
||
|
data := []byte("foobar")
|
||
|
payload, err := r.verifyJWT(signPayload(t, tc.key, data))
|
||
|
if err != nil {
|
||
|
if !tc.wantErr {
|
||
|
t.Fatalf("failed to verify JWT signed by %s: %v", tc.key, err)
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if tc.wantErr {
|
||
|
t.Fatalf("didn't expecte to be able to verify payload with %s", tc.key)
|
||
|
}
|
||
|
|
||
|
if bytes.Compare(payload, data) != 0 {
|
||
|
t.Errorf("want %q got %q", data, payload)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestCacheControl(t *testing.T) {
|
||
|
tests := []struct {
|
||
|
transport http.RoundTripper
|
||
|
expectedMaxAge time.Duration
|
||
|
url string
|
||
|
nKeys int
|
||
|
}{
|
||
|
{
|
||
|
transport: new(googleKeysRoundTripper),
|
||
|
expectedMaxAge: 18213 * time.Second,
|
||
|
url: "https://googleapis.com/oauth2/v3/certs",
|
||
|
nKeys: 4,
|
||
|
},
|
||
|
{
|
||
|
// SalesForce insists on not caching keys.
|
||
|
transport: new(salesForceKeysRoundTripper),
|
||
|
expectedMaxAge: minCache,
|
||
|
url: "https://login.salesforce.com/id/keys",
|
||
|
nKeys: 8,
|
||
|
},
|
||
|
}
|
||
|
for _, tc := range tests {
|
||
|
client := &http.Client{Transport: tc.transport}
|
||
|
|
||
|
before := time.Now().Add(tc.expectedMaxAge)
|
||
|
keys, expiry, err := requestKeys(client, tc.url)
|
||
|
if err != nil {
|
||
|
t.Fatalf("Request keys failed: %v", err)
|
||
|
}
|
||
|
tAfter := time.Now()
|
||
|
after := tAfter.Add(tc.expectedMaxAge)
|
||
|
approxExpiration := expiry.Sub(tAfter)
|
||
|
|
||
|
if expiry.Before(before) || expiry.After(after) {
|
||
|
t.Errorf("expected keys to expire in %s: got about %s", tc.expectedMaxAge, approxExpiration)
|
||
|
}
|
||
|
if len(keys) != tc.nKeys {
|
||
|
t.Errorf("expected %d keys got %d", tc.nKeys, len(keys))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var googleKeysReq = http.Request{
|
||
|
Method: "GET",
|
||
|
URL: &url.URL{Scheme: "https", Host: "www.googleapis.com", Path: "/oauth2/v3/certs"},
|
||
|
Proto: "HTTP/1.1",
|
||
|
ProtoMajor: 1,
|
||
|
ProtoMinor: 1,
|
||
|
Header: http.Header{
|
||
|
"Host": []string{"www.googleapis.com"},
|
||
|
"User-Agent": []string{"curl/7.43.0"},
|
||
|
"Accept": []string{"*/*"},
|
||
|
},
|
||
|
Host: "www.googleapis.com",
|
||
|
}
|
||
|
|
||
|
type googleKeysRoundTripper struct{}
|
||
|
|
||
|
func (_ *googleKeysRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||
|
return &http.Response{
|
||
|
Status: "200 OK",
|
||
|
StatusCode: 200,
|
||
|
Proto: "HTTP/1.1",
|
||
|
ProtoMajor: 1,
|
||
|
ProtoMinor: 1,
|
||
|
Header: http.Header{
|
||
|
"Expires": []string{"Sat, 16 Jul 2016 01:27:10 GMT"},
|
||
|
"Date": []string{"Fri, 15 Jul 2016 20:23:37 GMT"},
|
||
|
"Vary": []string{"Origin", "X-Origin"},
|
||
|
"Content-Type": []string{"application/json; charset=UTF-8"},
|
||
|
"X-Content-Type-Options": []string{"nosniff"},
|
||
|
"X-Frame-Options": []string{"SAMEORIGIN"},
|
||
|
"X-XSS-Protection": []string{"1; mode=block"},
|
||
|
"Content-Length": []string{"1957"},
|
||
|
"Server": []string{"GSE"},
|
||
|
"Cache-Control": []string{"public, max-age=18213, must-revalidate, no-transform"},
|
||
|
"Age": []string{"13156"},
|
||
|
"Alternate-Protocol": []string{"443:quic"},
|
||
|
"Alt-Svc": []string{`quic=":443"; ma=2592000; v="36,35,34,33,32,31,30,29,28,27,26,25"`},
|
||
|
},
|
||
|
ContentLength: 1957,
|
||
|
Body: ioutil.NopCloser(strings.NewReader(googleKeysBody)),
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
type salesForceKeysRoundTripper struct{}
|
||
|
|
||
|
func (_ *salesForceKeysRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||
|
return &http.Response{
|
||
|
Status: "200 OK",
|
||
|
StatusCode: 200,
|
||
|
Proto: "HTTP/1.1",
|
||
|
ProtoMajor: 1,
|
||
|
ProtoMinor: 1,
|
||
|
Header: http.Header{
|
||
|
"Date": []string{"Mon, 18 Jul 2016 23:06:26 GMT"},
|
||
|
"Strict-Transport-Security": []string{"max-age=10886400; includeSubDomains; preload"},
|
||
|
"Content-Security-Policy-Report-Only": []string{"default-src https:; script-src https: 'unsafe-inline' 'unsafe-eval'; style-src https: 'unsafe-inline'; img-src https: data:; font-src https: data:; report-uri /_/ContentDomainCSPNoAuth?type=login"},
|
||
|
"Set-Cookie": []string{"BrowserId=YQwZPagMRDiCEqn6s7D4cg;Path=/;Domain=.salesforce.com;Expires=Fri, 16-Sep-2016 23:06:26 GMT"},
|
||
|
"Expires": []string{"Thu, 01 Jan 1970 00:00:00 GMT"},
|
||
|
"Content-Type": []string{"application/json;charset=UTF-8"},
|
||
|
"Pragma": []string{"no-cache"},
|
||
|
"Cache-Control": []string{"no-cache, no-store"},
|
||
|
"Transfer-Encoding": []string{"chunked"},
|
||
|
},
|
||
|
ContentLength: -1,
|
||
|
Body: ioutil.NopCloser(strings.NewReader(salesForceKeysBody)),
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
var googleKeysBody = `{
|
||
|
"keys": [
|
||
|
{
|
||
|
"kty": "RSA",
|
||
|
"alg": "RS256",
|
||
|
"use": "sig",
|
||
|
"kid": "40aa42edac0614d7ca3f57f97ee866cdfba3b61a",
|
||
|
"n": "6lm9AEGLPFpVqnfeVFuTIZsj7vz_kxla6uW1WWtosM_MtIjXkyyiSolxiSOs3bzG66iVm71023QyOzKYFbio0hI-yZauG3g9nH-zb_AHScsjAKagHtrHmTdtq0JcNkQnAaaUwxVbjwMlYAcOh87W5jWj_MAcPvc-qjy8-WJ81UgoOUZNiKByuF4-9igxKZeskGRXuTPX64kWGBmKl-tM7VnCGMKoK3m92NPrktfBoNN_EGGthNfQsKFUdQFJFtpMuiXp9Gib7dcMGabxcG2GUl-PU086kPUyUdUYiMN2auKSOxSUZgDjT7DcI8Sn8kdQ0-tImaHi54JNa1PNNdKRpw",
|
||
|
"e": "AQAB"
|
||
|
},
|
||
|
{
|
||
|
"kty": "RSA",
|
||
|
"alg": "RS256",
|
||
|
"use": "sig",
|
||
|
"kid": "8fbbeea40332d2c0d27e37e1904af29b64594e57",
|
||
|
"n": "z7h6_rt35-j6NV2iQvYIuR3xvsxmEImgMl8dc8CFl4SzEWrry3QILajKxQZA9YYYfXIcZUG_6R6AghVMJetNIl2AhCoEr3RQjjNsm9PE6h5p2kQ-zIveFeb__4oIkVihYtxtoYBSdVj69nXLUAJP2bxPfU8RDp5X7hT62pKR05H8QLxH8siIQ5qR2LGFw_dJcitAVRRQofuaj_9u0CLZBfinqyRkBc7a0zi7pBxtEiIbn9sRr8Kkb_Boap6BHbnLS-YFBVarcgFBbifRf7NlK5dqE9z4OUb-dx8wCMRIPVAx_hV4Qx2anTgp1sDA6V4vd4NaCOZX-mSctNZqQmKtNw",
|
||
|
"e": "AQAB"
|
||
|
},
|
||
|
{
|
||
|
"kty": "RSA",
|
||
|
"alg": "RS256",
|
||
|
"use": "sig",
|
||
|
"kid": "6758b0b8eb341e90454860432d6a1648bf4de03b",
|
||
|
"n": "5K0rYaA7xtqSe1nFn_nCA10uUXY81NcohMeFsYLbBlx_NdpsmbpgtXJ6ektYR7rUdtMMLu2IONlNhkWlx-lge91okyacUrWHP88PycilUE-RnyVjbPEm3seR0VefgALfN4y_e77ljq2F7W2_kbUkTvDzriDIWvQT0WwVF5FIOBydfDDs92S-queaKgLBwt50SXJCZryLew5ODrwVsFGI4Et6MLqjS-cgWpCNwzcRqjBRsse6DXnex_zSRII4ODzKIfX4qdFBKZHO_BkTsK9DNkUayrr9cz8rFRK6TEH6XTVabgsyd6LP6PTxhpiII_pTYRSWk7CGMnm2nO0dKxzaFQ",
|
||
|
"e": "AQAB"
|
||
|
},
|
||
|
{
|
||
|
"kty": "RSA",
|
||
|
"alg": "RS256",
|
||
|
"use": "sig",
|
||
|
"kid": "0b915c6a65e2651ddcd0977757ebc644220a23b5",
|
||
|
"n": "vKa1quTzaKez5hyWc1SRYA_pgCEUx2FgpcfHEz33IjruqM57QTg4jZ2sukU5JdkjegtZm_ry9FuFKB-tIFszNqRBc4hgKWNzvaXicPpGP-tWFxhe30esaXUhF4WpZd4uhLfowJxuXKkM0qjRFAbQiP_N64fauozjquLfESaT0WdclK-wACzb-Mo9GoxYzgmPSRvmNJ83ZfBOimcIfuCQZkIFUjHrYRJ-kZfS02_tkkqpP8KaU3QL0igsQglawJpvH3TTZbA0xuJMOwGESN1xq4Xr_7o3OfNTTfybdVv1o8FMT8snIATGwKXvi9J3P7OEV6r8_pdFUhCOAvRlUYw5-Q",
|
||
|
"e": "AQAB"
|
||
|
}
|
||
|
]
|
||
|
}`
|
||
|
|
||
|
var salesForceKeysBody = `{"keys":[{"kty":"RSA","n":"nOcQOvHV8rc-hcfP_RmxMGjyVlruSLeFXTojYcbixaAH36scUejjaws31orUjmYqB5isE9ntdsL4DnsdP_MDJ2mtYD2FIh8tBkJjgXitjdcDclrwELAx846wBIlSES8wR6czpdJZfSwhL_92EGpDH6z7lKEClqhDlbtZ-yFKFj9BQRwaEXWV7uuq23gxXOqyEN0WXl3ZJPgsodCnlXRn9y_r5CNV9V4wvzXGlJhT3Nv_N_Z5XNZIjZnHdCuE_itT4a1xENEEds7Jjg5mRTlVFzYv5iQtBo7jdY5ogMTgKPmRh6hYuqLeki3AOAUff1AGaN9TZH60UxwTw03-DQJL5C2SuC_vM5KIWxZxQniubfegUCBXpJSAJbLt8zSFztTcrLS4-wgUHo1A8TDNaO28_KsBUTWsrieOr3NfCn4bPNb7t8G90U60lW0GIhEda3fNYnV0WWpZVO1jCRNy_JYUs3ECo0E1ZQJZD72Dm6UjiuH7eR3ZgNKR9tlLNdyZSpZUZPErLrXJ90d5XbmJYvRX9r93z6GQqOv5FQy1JhatwefxhKdyhkDEHsqELO0XDqnDnmgxkEEU-lHYSVGz-iDlUZOUYTTCtxsPDmBIXOMuwp0UydJphO36qRQaDyEjHNsYKLj5KVvjDHS8Gw1FhbFvsoUrBHre4hLY9Pa5meatV_k","e":"AQAB","alg":"RS256","use":"sig","kid":"192"},{"kty":"RSA","n":"sTuRuR0_kfPcrMug1dg4e3y4_elyaKDyzqlvEpzo6FgYTAuR-G9wv4Au6O363PDF8Yd8wg5JSwLjcHlfp3auXb_9pzJyhMBUAQwUYhVOPyxGOiyGYwCwkUX_GUA8IGPQZ6XOISX65492upv2DXW85vxYjPGJypWIQ3GjYrYtqmEg2fkuXX3IwITMxHoxYSmvR5Qcn4acnxRKMqCohxLANDEN4xyZBGbuchXnb0TGFwcGNAO8mz-O4E_A9k3QQwz750Qz6xEe8_UWPRvyX32EvGN19tX90DcT488YT0ImdUt6sVZEyOU_vxYXFFpWhf2_C1_AcjUQejex5y4mQn3idtVSAVGQ_eqhS3ZpyY_IhAGSC1JR_TJfrgnJoQrvv7Toz4K-iskOkFDNZYjhkKSla0EaJOUjRdZxmFTlzydmJFCD16-dbB8cCYGEQWVhnmBnYRQ2zquTCGReuILEcV0ALo9sR7zA5u8xSoJpZO1-KM_9lfbBH5M8V6R-ECWJI5MqPJxK0D3BCiNNxI5Kdslb_49SNA3qukQciassa-xbm26MGyYjt7Kn2KC2Tdh8L0z3328Pe6Wn7-vixSbtSc_qy15OifYKHr7BE1JjZfbkFQ6ZY5WgHZOZWG31-9aIhKsCKJGfGwyaN-I2-IzygSIihJNrFnSrO_s80li3H8P-4A0","e":"AQAB","alg":"RS256","use":"sig","kid":"198"},{"kty":"RSA","n":"n9ggEbWxKGy-cke0GpNQlblk47vL7-rtSsPwehgCdt7vANf6n6eqopLN1WPfq-xRJo7aE4gBXV3hQ4Ts1vm3_lXZmUCcFl8iVEYtDUZakmCYlDZExpo5AuRpwIayaidOzFP2qpMNstd9iVN_0ShCNjN-I_kJXAliqxuEZw5A-P-mcUBkE5ar_xVV3TaAXsxU5cecou9fbQjSLSKpQzfKVdUr0japx4iwSv0vdjXzItUfu7CykHU03Xmd4ucv47gdE0FeESgtPvC6cz6W6qCddP3rjBlgYAg0xYu_uyfLLYyaZZoEx8PfYUXfC4zM2a9p7bT1RxfRJDIqA5S507CylpzeAdBdp0N9UCtecENVgVcRotFBv1GOuVEhkba93spdddCGTSjWqzbq2hVj4napeGZ3I1st0mLQCD2reUh6injMFkVllnpfOitjvM98V-c_cxiTJYfg8K338atpWM7nR9Yo5D684Pr-z77xTl0_WY8mIukmGQtznX2dEYYbyzj_PLtK4YSPyrvnqdGFcZpxstSWOHnCeLNigzWCCBbcO2B8F3QGDagW6Zu7IH_P3KRPqwKWDzGzpmDJPGYexz46qezjVnPm-aCcx_m6cnqXqMMt60YtCMFCLxyyBJrTx66LNUpxVRun2vAu2HHdwxv1lzrEvDMzwrSW9JZVmAJ_eg0","e":"AQAB","alg":"RS256","use":"sig","kid":"202"},{"kty":"RSA","n":"o8Fz0jXjZ0Rz5Kt2TmzP0xVokf-Q4Az-MQg5i5MCxNNTQiZp7VkwAZeM0mJ-mKDbCzPm9ws43v8cxeiIkVZQqrAocnnb90MDCnU-7oD7MvOU4SbmhuLzVCyVZPIBRq5z0OgjcwLeD4trOoogkLOu0kyuyzNoFkr712m_GZ1xic-X0MlFKq3-2cI4U2nEuuh-Xcy7bUqCx0zTJFPOOKghGYEZZ6biZ04VC-ERcW6cC19pEWm6vCqZJEsKPCfazVAoHKZAukNd0XLPQd_W6xAaGnp8e7a5tFHn6dU6ikhI94ZieVp6WItWsQTDwJH-D7bVpVRG-lWL74lgcuQdFAtldm__k7FvlTXdqiLrd0rYuDnTFiwUSsUXWBJbmGVsEOylZVPQAL-K7G7p3BRY4X26vOgfludwCOj7L7WFbd0IXziTm74xe2KZGKsFpoCjJI0z_D5Oe5bofswr1Ceafhl97suG7OoInobt7QAQnnLcBVzUPz_TflOXDc5UiePptA0bxdd8MVENiDbTGGNz6DCzfL986QfcJLPB8aZa3lFN0kWPBkOclZagL4WpyIllB6euvZ2hfpt8IY2_bmUN06luo6N7Fy0hSSFMWvfzaD8_Ff3czb1Kv-b0xI6Ugk4d67RNNSbTcRM2Muvx-dJgOyXqrc_hE96OOqcMjrGZJoXnCAM","e":"AQAB","alg":"RS256","use":"sig","kid":"190"},{"kty":"RSA","n":"wIQtK09qsu1qCCQu1mHh6d_EyyOlbqMCV8WMacOyhZng1sbaFJY-0PIH46Kw1uhjbHg94_r2UELYd30vF8xwViGhCmpPuSGhkxNoT5CMoJPS6JW-zBpR7suHqBUnaGdZ6G2uYZDpwWYs_4SJDuWzxVBrQqIM_ZVgUqutniQPmjMAX5MqznBTnG3zm728BmNzS7T2gtzxs3jAgDsSAu3Kxp3D6NDGERhaAJ8jOgwHvmQK5xFi9Adw7sv2nCH-wM-C5fLJYmpGOSrTP1HLOlq--TROAvWL9gcNEeq4arryIYux5syg66rHT8U2Uhb1PdXt7ReQY8wBnP2BBH1QH7rzOZ7UbqFLbQUQsZFAVMcfm7gJN8JWLlcSJZdC2zaY0wI5q8PWN-N_GgAK64FKZQ7pB0bRQ5AQx-D3U4sYE4EcgSvV8fW86PaF1VXaHMFcom48gZ1GzE_V25uPb-0yue0cv9lejrIKDvRiJ5UiyUPphro4Aw2ZcDi_8r8rqfglWhcnB4bGSri4kEBb_IdwvqKwRCqxlNdRnU1ooQeUBaVRwdbpj23Z1qtYjB55Wf2KOCJ6ewMyddq4bEAG6KIqPmssT7_exvygUyuW6qhnCV-gTZEwFI0A6djsHM5itfkzNY47BeuAtGXjuaRnVYIEvTrnSj3Lx7YfvCIiGqFrG6y31Ak","e":"AQAB","alg":"RS256","use":"sig","kid":"188"},{"kty":"RSA","n":"hsqiqMXZmxJHzWfZwbSffKfc9YYMxj83-aWhA91jtI8k-GMsEB6mtoNWLP6vmz6x6BQ8Sn6kmn65n1IGCIlWxhPn9yqfXBDBaHFGYED9bBloSEMFnnS9-ACsWrHl5UtDQ3nh-VQTKg1LBmjJMmAOHdBLoUikfpx8fjA1LfDn_1iNWnguj2ehgjWCuTn64UdUd84YNcfO8Ha0TAhWHOhkiluMyzGS0dtN0h8Ybyi5oL6Bf1sfhtOncUh1JuWMcmvICbGEkA_0vBbMp9nCvXdMl
|