Skip to content

Commit

Permalink
Fix issuer url and add handling of 404 from old registries
Browse files Browse the repository at this point in the history
Previously I wasn't using the term "issuer" correctly -- really, we should be thinking
about the issuer as the endpoint against which we perform openID-configuration lookup
to find the issuer key. That is, for registry.com and ns prefix /foo/bar, the issuer
is registry.com/api/v1.0/registry/foo/bar. We take that endpoint and grab the JSON
hosted at registry.com/api/v1.0/registry/foo/bar/.well-known/openid-configuration.
From there, most Pelican services will point to the key hosted at
registry.com/api/v1.0/registry/foo/bar/.well-known/issuer.jwks.
  • Loading branch information
jhiemstrawisc committed Jan 31, 2024
1 parent fd92230 commit 22b79fe
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 31 deletions.
67 changes: 48 additions & 19 deletions director/origin_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ func VerifyAdvertiseToken(ctx context.Context, token, namespace string) (bool, e
return false, err
}

keyLoc, err := GetJWKSURLFromIssuerURL(issuerUrl)
if err != nil {
return false, err
}

var ar NamespaceCache

// defer statements are scoped to function, not lexical enclosure,
Expand All @@ -137,7 +142,7 @@ func VerifyAdvertiseToken(ctx context.Context, token, namespace string) (bool, e
if ar == nil {
ar = jwk.NewCache(ctx)
client := &http.Client{Transport: config.GetTransport()}
if err = ar.Register(issuerUrl, jwk.WithMinRefreshInterval(15*time.Minute), jwk.WithHTTPClient(client)); err != nil {
if err = ar.Register(keyLoc, jwk.WithMinRefreshInterval(15*time.Minute), jwk.WithHTTPClient(client)); err != nil {
return false, err
}
namespaceKeysMutex.Lock()
Expand All @@ -151,8 +156,8 @@ func VerifyAdvertiseToken(ctx context.Context, token, namespace string) (bool, e
}

}
log.Debugln("Attempting to fetch keys from ", issuerUrl)
keyset, err := ar.Get(ctx, issuerUrl)
log.Debugln("Attempting to fetch keys from ", keyLoc)
keyset, err := ar.Get(ctx, keyLoc)

if log.IsLevelEnabled(log.DebugLevel) {
// Let's check that we can convert to JSON and get the right thing...
Expand Down Expand Up @@ -274,9 +279,9 @@ func VerifyDirectorTestReportToken(strToken string) (bool, error) {
return false, nil
}

// For a given prefix, get the url of the issuer/public key from the registry
// This works by looking up the namespace-configuration json for the namespace
// and grabbing the value corresponding to the "jwks_uri" key.
// For a given prefix, get the prefix's issuer URL, where we consider that the openid endpoint
// we use to look up a key location. Note that this is NOT the same as the issuer key -- to
// find that, follow openid-style discovery using the issuer URL as a base.
func GetNSIssuerURL(prefix string) (string, error) {
if prefix == "" || !strings.HasPrefix(prefix, "/") {
return "", errors.New(fmt.Sprintf("the prefix \"%s\" is invalid", prefix))
Expand All @@ -290,33 +295,57 @@ func GetNSIssuerURL(prefix string) (string, error) {
return "", err
}

registryUrl.Path, err = url.JoinPath(registryUrl.Path, "api", "v1.0", "registry", prefix, ".well-known", "openid-configuration")
registryUrl.Path, err = url.JoinPath(registryUrl.Path, "api", "v1.0", "registry", prefix)

if err != nil {
return "", errors.Wrapf(err, "failed to construct namespace-configuration lookup URL for prefix %s", prefix)
return "", errors.Wrapf(err, "failed to construct openid-configuration lookup URL for prefix %s", prefix)
}
return registryUrl.String(), nil
}

// Given an issuer url, lookup the JWKS URL from the openid-configuration
// For example, if the issuer URL is https://registry.com:8446/api/v1.0/registry/test-namespace,
// this function will return the key indicated by the openid-configuration JSON hosted at
// https://registry.com:8446/api/v1.0/registry/test-namespace/.well-known/openid-configuration.
func GetJWKSURLFromIssuerURL(issuerUrl string) (string, error) {
// Get/parse the openid-configuration JSON to lookup key location
issOpenIDUrl, err := url.Parse(issuerUrl)
if err != nil {
return "", errors.Wrap(err, "failed to parse issuer URL")
}
issOpenIDUrl.Path, _ = url.JoinPath(issOpenIDUrl.Path, ".well-known", "openid-configuration")

// Get/parse the namespace-configuration JSON to lookup key location
client := &http.Client{Transport: config.GetTransport()}
originConfig, err := client.Get(registryUrl.String())
openIDCfg, err := client.Get(issOpenIDUrl.String())
if err != nil {
return "", errors.Wrapf(err, "failed to lookup namespace configuration for prefix %s", prefix)
return "", errors.Wrapf(err, "failed to lookup openid-configuration for issuer %s", issuerUrl)
}
defer openIDCfg.Body.Close()

// If we hit an old registry, it may not have the openid-configuration. In that case, we fallback to the old
// behavior of looking for the key directly at the issuer URL.
if openIDCfg.StatusCode == http.StatusNotFound {
oldKeyLoc, err := url.JoinPath(issuerUrl, ".well-known", "issuer.jwks")
if err != nil {
return "", errors.Wrapf(err, "failed to construct key lookup URL for issuer %s", issuerUrl)
}
return oldKeyLoc, nil
}
defer originConfig.Body.Close()

body, err := io.ReadAll(originConfig.Body)
body, err := io.ReadAll(openIDCfg.Body)
if err != nil {
return "", errors.Wrapf(err, "failed to read response body from %s", registryUrl.String())
return "", errors.Wrapf(err, "failed to read response body from %s", issuerUrl)
}

var originCfgMap map[string]string
err = json.Unmarshal(body, &originCfgMap)
var openIDCfgMap map[string]string
err = json.Unmarshal(body, &openIDCfgMap)
if err != nil {
return "", errors.Wrapf(err, "failed to unmarshal namespace configuration for prefix %s", prefix)
return "", errors.Wrapf(err, "failed to unmarshal openid-configuration for issuer %s", issuerUrl)
}

if keyLoc, ok := originCfgMap["jwks_uri"]; ok {
if keyLoc, ok := openIDCfgMap["jwks_uri"]; ok {
return keyLoc, nil
} else {
return "", errors.New(fmt.Sprintf("no key found in namespace configuration for prefix %s", prefix))
return "", errors.New(fmt.Sprintf("no key found in openid-configuration for issuer %s", issuerUrl))
}
}
27 changes: 15 additions & 12 deletions director/origin_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,23 +186,26 @@ func TestCreateAdvertiseToken(t *testing.T) {

func TestGetNSIssuerURL(t *testing.T) {
viper.Reset()
viper.Set("Federation.RegistryUrl", "https://registry.com:8446")
url, err := GetNSIssuerURL("/test-prefix")
assert.Equal(t, nil, err)
assert.Equal(t, "https://registry.com:8446/api/v1.0/registry/test-prefix", url)
viper.Reset()
}

emptyRegistry := registryMockup(t, "")
defer emptyRegistry.Close()

viper.Set("Federation.RegistryUrl", emptyRegistry.URL)
// No namespace url has been set, so an error is expected
url, err := GetNSIssuerURL("")
assert.Equal(t, "the prefix \"\" is invalid", err.Error())
assert.Equal(t, "", url)

// Test to make sure the path is as expected
func TestGetJWKSURLFromIssuerURL(t *testing.T) {
viper.Reset()
registry := registryMockup(t, "/test-prefix")
defer registry.Close()
viper.Set("Federation.RegistryUrl", registry.URL)
url, err = GetNSIssuerURL("/test-prefix")
expectedIssuerUrl := registry.URL + "/api/v1.0/registry/test-prefix"
url, err := GetNSIssuerURL("/test-prefix")
assert.Equal(t, nil, err)
assert.Equal(t, expectedIssuerUrl, url)

keyLoc, err := GetJWKSURLFromIssuerURL(url)
assert.Equal(t, nil, err)
assert.Equal(t, "https://registry.com:8446/api/v1.0/registry/test-prefix/.well-known/issuer.jwks", url)
assert.Equal(t, "https://registry.com:8446/api/v1.0/registry/test-prefix/.well-known/issuer.jwks", keyLoc)
}

func TestNamespaceKeysCacheEviction(t *testing.T) {
Expand Down

0 comments on commit 22b79fe

Please sign in to comment.