This repository has been archived by the owner on Jul 12, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 83
/
jwks.go
147 lines (130 loc) · 4.41 KB
/
jwks.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
// Copyright 2020 the Exposure Notifications Verification Server authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package jwks handles returning JSON encoded information about the
// server's encryption keys. See https://tools.ietf.org/html/rfc75170
// for information about the server.
package jwks
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/google/exposure-notifications-verification-server/pkg/cache"
"github.com/google/exposure-notifications-verification-server/pkg/controller"
"github.com/google/exposure-notifications-verification-server/pkg/database"
"github.com/google/exposure-notifications-verification-server/pkg/keyutils"
"github.com/google/exposure-notifications-verification-server/pkg/render"
"github.com/gorilla/mux"
"github.com/rakutentech/jwk-go/jwk"
)
// Controller holds all the pieces necessary to show the jwks encoded keys.
type Controller struct {
h *render.Renderer
db *database.Database
keyCache *keyutils.PublicKeyCache
cacher cache.Cacher
}
// HandleIndex returns an http.Handler that handles jwks GET requests.
func (c *Controller) HandleIndex() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// key is the key in the cacher where the values for this JWK are cached.
key := &cache.Key{
Namespace: "jwks",
Key: strings.ToLower(r.URL.String()),
}
// See if there's a cached value. Note we cannot use Fetch here because our
// fetch function also depends on the cacher to lookup pubic keys and
// results in a deadlock.
var encoded []*jwk.JWK
if err := c.cacher.Read(ctx, key, &encoded); err != nil {
if !errors.Is(err, cache.ErrNotFound) {
controller.InternalError(w, r, c.h, err)
return
}
// Fall-through to lookup logic
} else {
c.h.RenderJSON(w, http.StatusOK, encoded)
return
}
// Grab the URL path components we need.
realmID := mux.Vars(r)["realm_id"]
// Find the realm, and the key abstractions from the DB.
realm, err := c.db.FindRealmByRegionOrID(realmID)
if err != nil {
if database.IsNotFound(err) {
c.h.RenderJSON(w, http.StatusNotFound, fmt.Errorf("no realm exists for region %q", realmID))
return
}
controller.InternalError(w, r, c.h, err)
return
}
// If we got this far, it means there was no cached value, so do a full
// read.
encoded, err = func() ([]*jwk.JWK, error) {
var keys []*database.SigningKey
keys, err = realm.ListSigningKeys(c.db)
if err != nil {
return nil, err
}
encoded := make([]*jwk.JWK, len(keys))
for i, key := range keys {
pk, err := c.keyCache.GetPublicKey(ctx, key.KeyID, c.db.KeyManager())
if err != nil {
return nil, err
}
// Encode it, and sent it off.
spec := jwk.NewSpec(pk)
spec.KeyID = key.GetKID()
encoded[i], err = spec.ToJWK()
if err != nil {
return nil, err
}
}
return encoded, nil
}()
if err != nil {
controller.InternalError(w, r, c.h, err)
return
}
// It's possible there were concurrent requests and someone already has the
// cache - now that we have the value, we can avoid the deadline and do a
// fetch. If there's already a cached value, our value will be discarded.
// Otherwise, it will be overwritten and saved in the cache.
ttl := 5 * time.Minute
if err := c.cacher.Fetch(ctx, key, &encoded, ttl, func() (interface{}, error) {
return encoded, nil
}); err != nil {
controller.InternalError(w, r, c.h, err)
return
}
// Get the keys.
c.h.RenderJSON(w, http.StatusOK, encoded)
})
}
// New creates a new jwks *Controller, and returns it.
func New(ctx context.Context, db *database.Database, cacher cache.Cacher, h *render.Renderer) (*Controller, error) {
kc, err := keyutils.NewPublicKeyCache(ctx, cacher, time.Minute)
if err != nil {
return nil, err
}
return &Controller{
h: h,
db: db,
keyCache: kc,
cacher: cacher,
}, nil
}