Skip to content

Commit

Permalink
Add library for OIDC token management (#7315)
Browse files Browse the repository at this point in the history
* 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
creydr authored Oct 4, 2023
1 parent ce67d85 commit 402f6ac
Show file tree
Hide file tree
Showing 47 changed files with 11,206 additions and 1 deletion.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/cloudevents/sdk-go/observability/opencensus/v2 v2.13.0
github.com/cloudevents/sdk-go/sql/v2 v2.13.0
github.com/cloudevents/sdk-go/v2 v2.13.0
github.com/coreos/go-oidc/v3 v3.6.0
github.com/golang/protobuf v1.5.3
github.com/google/go-cmp v0.5.9
github.com/google/gofuzz v1.2.0
Expand Down Expand Up @@ -70,6 +71,7 @@ require (
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.7.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-logr/logr v1.2.4 // indirect
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ github.com/cloudevents/sdk-go/v2 v2.13.0/go.mod h1:xDmKfzNjM8gBvjaF8ijFjM1VYOVUE
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o=
github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
Expand Down Expand Up @@ -122,6 +124,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
Expand Down Expand Up @@ -465,6 +469,7 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
Expand Down
2 changes: 1 addition & 1 deletion hack/create-kind-cluster.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ kubeadmConfigPatches:
kind: ClusterConfiguration
apiServer:
extraArgs:
"service-account-issuer": "kubernetes.default.svc"
"service-account-issuer": "https://kubernetes.default.svc"
"service-account-signing-key-file": "/etc/kubernetes/pki/sa.key"
networking:
dnsDomain: "${CLUSTER_SUFFIX}"
Expand Down
67 changes: 67 additions & 0 deletions pkg/auth/token_provider.go
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
}
160 changes: 160 additions & 0 deletions pkg/auth/token_verifier.go
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"`
}
Loading

0 comments on commit 402f6ac

Please sign in to comment.