-
Notifications
You must be signed in to change notification settings - Fork 593
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add library for OIDC token management (#7315)
* Add token provider lib * Add github.com/coreos/go-oidc * Add token verifier lib * Fix KinD setup with valid service account issuer
- Loading branch information
Showing
47 changed files
with
11,206 additions
and
1 deletion.
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
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
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
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,67 @@ | ||
/* | ||
Copyright 2023 The Knative 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 auth | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"go.uber.org/zap" | ||
authv1 "k8s.io/api/authentication/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/types" | ||
"k8s.io/client-go/kubernetes" | ||
kubeclient "knative.dev/pkg/client/injection/kube/client" | ||
"knative.dev/pkg/logging" | ||
) | ||
|
||
type OIDCTokenProvider struct { | ||
logger *zap.SugaredLogger | ||
kubeClient kubernetes.Interface | ||
} | ||
|
||
func NewOIDCTokenProvider(ctx context.Context) *OIDCTokenProvider { | ||
tokenProvider := &OIDCTokenProvider{ | ||
logger: logging.FromContext(ctx).With("component", "oidc-token-provider"), | ||
kubeClient: kubeclient.Get(ctx), | ||
} | ||
|
||
return tokenProvider | ||
} | ||
|
||
// GetJWT returns a JWT from the given service account for the given audience. | ||
func (c *OIDCTokenProvider) GetJWT(serviceAccount types.NamespacedName, audience string) (string, error) { | ||
// TODO: check the cache | ||
|
||
// if not found in cache: request new token | ||
tokenRequest := authv1.TokenRequest{ | ||
Spec: authv1.TokenRequestSpec{ | ||
Audiences: []string{audience}, | ||
}, | ||
} | ||
|
||
tokenRequestResponse, err := c.kubeClient. | ||
CoreV1(). | ||
ServiceAccounts(serviceAccount.Namespace). | ||
CreateToken(context.TODO(), serviceAccount.Name, &tokenRequest, metav1.CreateOptions{}) | ||
|
||
if err != nil { | ||
return "", fmt.Errorf("could not request a token for %s: %w", serviceAccount, err) | ||
} | ||
|
||
return tokenRequestResponse.Status.Token, nil | ||
} |
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,160 @@ | ||
/* | ||
Copyright 2023 The Knative 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 auth | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/coreos/go-oidc/v3/oidc" | ||
"go.uber.org/zap" | ||
"k8s.io/client-go/rest" | ||
"knative.dev/eventing/pkg/apis/feature" | ||
"knative.dev/pkg/injection" | ||
"knative.dev/pkg/logging" | ||
) | ||
|
||
const ( | ||
kubernetesOIDCDiscoveryBaseURL = "https://kubernetes.default.svc" | ||
) | ||
|
||
type OIDCTokenVerifier struct { | ||
logger *zap.SugaredLogger | ||
restConfig *rest.Config | ||
provider *oidc.Provider | ||
} | ||
|
||
type IDToken struct { | ||
Issuer string | ||
Audience []string | ||
Subject string | ||
Expiry time.Time | ||
IssuedAt time.Time | ||
AccessTokenHash string | ||
} | ||
|
||
func NewOIDCTokenVerifier(ctx context.Context) *OIDCTokenVerifier { | ||
tokenHandler := &OIDCTokenVerifier{ | ||
logger: logging.FromContext(ctx).With("component", "oidc-token-handler"), | ||
restConfig: injection.GetConfig(ctx), | ||
} | ||
|
||
if err := tokenHandler.initOIDCProvider(ctx); err != nil { | ||
tokenHandler.logger.Error(fmt.Sprintf("could not initialize provider. You can ignore this message, when the %s feature is disabled", feature.OIDCAuthentication), zap.Error(err)) | ||
} | ||
|
||
return tokenHandler | ||
} | ||
|
||
// VerifyJWT verifies the given JWT for the expected audience and returns the parsed ID token. | ||
func (c *OIDCTokenVerifier) VerifyJWT(ctx context.Context, jwt, audience string) (*IDToken, error) { | ||
if c.provider == nil { | ||
return nil, fmt.Errorf("provider is nil. Is the OIDC provider config correct?") | ||
} | ||
|
||
verifier := c.provider.Verifier(&oidc.Config{ | ||
ClientID: audience, | ||
}) | ||
|
||
token, err := verifier.Verify(ctx, jwt) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not verify JWT: %w", err) | ||
} | ||
|
||
return &IDToken{ | ||
Issuer: token.Issuer, | ||
Audience: token.Audience, | ||
Subject: token.Subject, | ||
Expiry: token.Expiry, | ||
IssuedAt: token.IssuedAt, | ||
AccessTokenHash: token.AccessTokenHash, | ||
}, nil | ||
} | ||
|
||
func (c *OIDCTokenVerifier) initOIDCProvider(ctx context.Context) error { | ||
discovery, err := c.getKubernetesOIDCDiscovery() | ||
if err != nil { | ||
return fmt.Errorf("could not load Kubernetes OIDC discovery information: %w", err) | ||
} | ||
|
||
if discovery.Issuer != kubernetesOIDCDiscoveryBaseURL { | ||
// in case we have another issuer as the api server: | ||
ctx = oidc.InsecureIssuerURLContext(ctx, discovery.Issuer) | ||
} | ||
|
||
httpClient, err := c.getHTTPClientForKubeAPIServer() | ||
if err != nil { | ||
return fmt.Errorf("could not get HTTP client with TLS certs of API server: %w", err) | ||
} | ||
ctx = oidc.ClientContext(ctx, httpClient) | ||
|
||
// get OIDC provider | ||
c.provider, err = oidc.NewProvider(ctx, kubernetesOIDCDiscoveryBaseURL) | ||
if err != nil { | ||
return fmt.Errorf("could not get OIDC provider: %w", err) | ||
} | ||
|
||
c.logger.Debug("updated OIDC provider config", zap.Any("discovery-config", discovery)) | ||
|
||
return nil | ||
} | ||
|
||
func (c *OIDCTokenVerifier) getHTTPClientForKubeAPIServer() (*http.Client, error) { | ||
client, err := rest.HTTPClientFor(c.restConfig) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not create HTTP client from rest config: %w", err) | ||
} | ||
|
||
return client, nil | ||
} | ||
|
||
func (c *OIDCTokenVerifier) getKubernetesOIDCDiscovery() (*openIDMetadata, error) { | ||
client, err := c.getHTTPClientForKubeAPIServer() | ||
if err != nil { | ||
return nil, fmt.Errorf("could not get HTTP client for API server: %w", err) | ||
} | ||
|
||
resp, err := client.Get(kubernetesOIDCDiscoveryBaseURL + "/.well-known/openid-configuration") | ||
if err != nil { | ||
return nil, fmt.Errorf("could not get response: %w", err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
body, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not read response body: %w", err) | ||
} | ||
|
||
openIdConfig := &openIDMetadata{} | ||
if err := json.Unmarshal(body, openIdConfig); err != nil { | ||
return nil, fmt.Errorf("could not unmarshall openid config: %w", err) | ||
} | ||
|
||
return openIdConfig, nil | ||
} | ||
|
||
type openIDMetadata struct { | ||
Issuer string `json:"issuer"` | ||
JWKSURI string `json:"jwks_uri"` | ||
ResponseTypes []string `json:"response_types_supported"` | ||
SubjectTypes []string `json:"subject_types_supported"` | ||
SigningAlgs []string `json:"id_token_signing_alg_values_supported"` | ||
} |
Oops, something went wrong.