-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit adds authorization via two methods: PSK and a Quay keyserver. The PSK simply uses a key specified in the configuration file. The keyserver authorization checking relies on the extra "kid" JWT header to look up a public key and validate the claims on the request. The tests for the keyserver integration are opportunistic and require passing a "keyserver" flag to the cmd/clair test for initial setup. Once initial setup is done, the key and server information will be cached and used opportunistically on future test runs.
- Loading branch information
Showing
12 changed files
with
786 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"strings" | ||
) | ||
|
||
// AuthCheck is an interface that reports whether the passed request should be | ||
// allowed to continue. | ||
type AuthCheck interface { | ||
Check(context.Context, *http.Request) bool | ||
} | ||
|
||
type authHandler struct { | ||
auth AuthCheck | ||
next http.Handler | ||
} | ||
|
||
func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
if !h.auth.Check(r.Context(), r) { | ||
w.WriteHeader(http.StatusUnauthorized) | ||
return | ||
} | ||
h.next.ServeHTTP(w, r) | ||
} | ||
|
||
// AuthHandler returns a Handler that gates access to the passed Handler behind | ||
// the passed AuthCheck. | ||
func AuthHandler(h http.Handler, f AuthCheck) http.Handler { | ||
return &authHandler{ | ||
auth: f, | ||
next: h, | ||
} | ||
} | ||
|
||
func fromHeader(r *http.Request) (string, bool) { | ||
hs, ok := r.Header["Authorization"] | ||
if !ok { | ||
return "", false | ||
} | ||
for _, h := range hs { | ||
if strings.HasPrefix(h, "Bearer ") { | ||
return strings.TrimPrefix(h, "Bearer "), true | ||
} | ||
} | ||
return "", false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"net/http" | ||
"net/url" | ||
"path" | ||
"strings" | ||
"sync" | ||
"time" | ||
|
||
"github.com/gregjones/httpcache" | ||
"gopkg.in/square/go-jose.v2" | ||
"gopkg.in/square/go-jose.v2/jwt" | ||
) | ||
|
||
type ks struct { | ||
root *url.URL | ||
client *http.Client | ||
mu sync.RWMutex | ||
cache map[string]*jose.JSONWebKey | ||
} | ||
|
||
// Check implements AuthCheck. | ||
func (s *ks) Check(ctx context.Context, r *http.Request) bool { | ||
wt, ok := fromHeader(r) | ||
if !ok { | ||
return false | ||
} | ||
tok, err := jwt.ParseSigned(wt) | ||
if err != nil { | ||
return false | ||
} | ||
aud, err := r.URL.Parse("/") | ||
if err != nil { | ||
return false | ||
} | ||
// Need to find the key id. | ||
ok = false | ||
var kid string | ||
for _, h := range tok.Headers { | ||
if h.Algorithm == string(jose.RS256) { | ||
ok = true | ||
kid = h.KeyID | ||
break | ||
} | ||
} | ||
if !ok { | ||
return false | ||
} | ||
// Need to pull out the issuer to fetch the key. We cannot return "true" | ||
// until *after* a safe Claims call succeeds. | ||
cl := jwt.Claims{} | ||
if err := tok.UnsafeClaimsWithoutVerification(&cl); err != nil { | ||
return false | ||
} | ||
uri, err := s.root.Parse(path.Join("./", "services", cl.Issuer, "keys", kid)) | ||
if err != nil { | ||
return false | ||
} | ||
ck := cl.Issuer + "+" + kid | ||
|
||
// This request will be cached according to the cache-control headers. | ||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "", nil) | ||
if err != nil { | ||
return false | ||
} | ||
req.URL = uri | ||
res, err := s.client.Do(req) | ||
if res != nil { | ||
defer res.Body.Close() | ||
} | ||
if err != nil { | ||
return false | ||
} | ||
if res.StatusCode != http.StatusOK { | ||
// If the keyserver returns a non-OK, we can't use the key: it doesn't | ||
// exist or is expired or is not yet approved, so make sure to delete it | ||
// from our cache. Delete is a no-op if we don't have the key. | ||
s.mu.Lock() | ||
delete(s.cache, ck) | ||
s.mu.Unlock() | ||
return false | ||
} | ||
s.mu.RLock() | ||
jwk, ok := s.cache[ck] | ||
s.mu.RUnlock() | ||
// If not in our deserialized cache or our response has been served from the | ||
// remote server, do the deserializtion and cache it. | ||
if !ok || res.Header.Get(httpcache.XFromCache) != "" { | ||
jwk = &jose.JSONWebKey{} | ||
if err := json.NewDecoder(res.Body).Decode(jwk); err != nil { | ||
return false | ||
} | ||
s.mu.Lock() | ||
// Only store if we didn't get beaten by another request. | ||
if _, ok := s.cache[ck]; !ok { | ||
s.cache[ck] = jwk | ||
} | ||
s.mu.Unlock() | ||
} | ||
|
||
if err := tok.Claims(jwk.Key, &cl); err != nil { | ||
return false | ||
} | ||
// Returning true is now possible. | ||
if err := cl.ValidateWithLeeway(jwt.Expected{ | ||
Audience: jwt.Audience{strings.TrimRight(aud.String(), "/")}, | ||
Time: time.Now(), | ||
}, 15*time.Second); err != nil { | ||
return false | ||
} | ||
return true | ||
} | ||
|
||
// QuayKeyserver returns an AuthCheck that validates JWTs by fetching keys from the | ||
// Quay at "api". | ||
// | ||
// It follows the algorithm outlined here: | ||
// https://github.com/quay/jwtproxy/tree/master/jwt/keyserver/keyregistry#verifier | ||
func QuayKeyserver(api string) (AuthCheck, error) { | ||
root, err := url.Parse(api) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
t := httpcache.NewMemoryCacheTransport() | ||
t.MarkCachedResponses = true | ||
return &ks{ | ||
client: t.Client(), | ||
root: root, | ||
cache: make(map[string]*jose.JSONWebKey), | ||
}, nil | ||
} |
Oops, something went wrong.