Skip to content

Commit 75f4f5e

Browse files
authored
feat: add function to generate oidc-based kubeconfig (#108)
* add function to generate oidc-based kubeconfig * make context and cluster name configurable in oidc kubeconfig
1 parent 741059f commit 75f4f5e

File tree

2 files changed

+232
-0
lines changed

2 files changed

+232
-0
lines changed

pkg/clusteraccess/access.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,170 @@ func ComputeTokenRenewalTimeWithRatio(creationTime, expirationTime time.Time, ra
344344
return renewalAt
345345
}
346346

347+
// CreateOIDCKubeconfig creates a kubeconfig that uses the oidc-login plugin for authentication.
348+
// The 'user' arg is used as key for the auth configuration and can be chosen freely.
349+
// Note that this kubeconfig is meant for human users, controllers can usually not execute 'kubectl oidc-login get-token'.
350+
func CreateOIDCKubeconfig(user, host string, caData []byte, issuer, clientID string, extraOptions ...CreateOIDCKubeconfigOption) ([]byte, error) {
351+
opts := &CreateOIDCKubeconfigOptions{
352+
User: user,
353+
Host: host,
354+
CAData: caData,
355+
Issuer: issuer,
356+
ClientID: clientID,
357+
ContextName: "cluster",
358+
ClusterName: "cluster",
359+
}
360+
361+
for _, apply := range extraOptions {
362+
apply(opts)
363+
}
364+
365+
return createOIDCKubeconfig(opts)
366+
}
367+
368+
func createOIDCKubeconfig(opts *CreateOIDCKubeconfigOptions) ([]byte, error) {
369+
grantType := opts.GrantType
370+
if grantType == "" {
371+
grantType = GrantTypeAuto
372+
}
373+
exec := &clientcmdapi.ExecConfig{
374+
APIVersion: "client.authentication.k8s.io/v1beta1",
375+
Command: "kubectl",
376+
Args: []string{
377+
"oidc-login",
378+
"get-token",
379+
"--grant-type=" + string(grantType),
380+
"--oidc-issuer-url=" + opts.Issuer,
381+
"--oidc-client-id=" + opts.ClientID,
382+
},
383+
}
384+
if opts.ClientSecret != "" {
385+
exec.Args = append(exec.Args, "--oidc-client-secret="+opts.ClientSecret)
386+
}
387+
for _, extraScope := range opts.ExtraScopes {
388+
exec.Args = append(exec.Args, "--oidc-extra-scope="+extraScope)
389+
}
390+
if opts.UsePKCE {
391+
exec.Args = append(exec.Args, "--oidc-use-pkce")
392+
}
393+
if opts.ForceRefresh {
394+
exec.Args = append(exec.Args, "--force-refresh")
395+
}
396+
397+
kcfg := clientcmdapi.Config{
398+
APIVersion: "v1",
399+
Kind: "Config",
400+
Clusters: map[string]*clientcmdapi.Cluster{
401+
opts.ClusterName: {
402+
Server: opts.Host,
403+
CertificateAuthorityData: opts.CAData,
404+
},
405+
},
406+
Contexts: map[string]*clientcmdapi.Context{
407+
opts.ContextName: {
408+
Cluster: opts.ClusterName,
409+
AuthInfo: opts.User,
410+
},
411+
},
412+
CurrentContext: opts.ContextName,
413+
AuthInfos: map[string]*clientcmdapi.AuthInfo{
414+
opts.User: {
415+
Exec: exec,
416+
},
417+
},
418+
}
419+
420+
kcfgBytes, err := clientcmd.Write(kcfg)
421+
if err != nil {
422+
return nil, fmt.Errorf("error converting converting generated kubeconfig into yaml: %w", err)
423+
}
424+
return kcfgBytes, nil
425+
}
426+
427+
type CreateOIDCKubeconfigOptions struct {
428+
ContextName string
429+
ClusterName string
430+
User string
431+
Host string
432+
CAData []byte
433+
Issuer string
434+
ClientID string
435+
ClientSecret string
436+
ExtraScopes []string
437+
UsePKCE bool
438+
ForceRefresh bool
439+
GrantType OIDCGrantType
440+
}
441+
442+
type OIDCGrantType string
443+
444+
const (
445+
GrantTypeAuto OIDCGrantType = "auto"
446+
GrantTypeAuthCode OIDCGrantType = "authcode"
447+
GrantTypeAuthCodeKeyboard OIDCGrantType = "authcode-keyboard"
448+
GrantTypePassword OIDCGrantType = "password"
449+
GrantTypeDeviceCode OIDCGrantType = "device-code"
450+
)
451+
452+
type CreateOIDCKubeconfigOption func(*CreateOIDCKubeconfigOptions)
453+
454+
// WithExtraScope is an option for CreateOIDCKubeconfig that adds an extra scope to the oidc-login subcommand.
455+
// This option can be used multiple times to add multiple scopes.
456+
func WithExtraScope(scope string) CreateOIDCKubeconfigOption {
457+
return func(opts *CreateOIDCKubeconfigOptions) {
458+
opts.ExtraScopes = append(opts.ExtraScopes, scope)
459+
}
460+
}
461+
462+
// UsePKCE is an option for CreateOIDCKubeconfig that enforces the use of PKCE.
463+
func UsePKCE() CreateOIDCKubeconfigOption {
464+
return func(opts *CreateOIDCKubeconfigOptions) {
465+
opts.UsePKCE = true
466+
}
467+
}
468+
469+
// ForceRefresh is an option for CreateOIDCKubeconfig that forces the refresh of the token, independent of its expiration time.
470+
func ForceRefresh() CreateOIDCKubeconfigOption {
471+
return func(opts *CreateOIDCKubeconfigOptions) {
472+
opts.ForceRefresh = true
473+
}
474+
}
475+
476+
// WithGrantType is an option for CreateOIDCKubeconfig that sets the grant type.
477+
// Valid values are "auto", "authcode", "authcode-keyboard", "password", and "device-code".
478+
func WithGrantType(grantType OIDCGrantType) CreateOIDCKubeconfigOption {
479+
return func(opts *CreateOIDCKubeconfigOptions) {
480+
opts.GrantType = grantType
481+
}
482+
}
483+
484+
// WithClientSecret is an option for CreateOIDCKubeconfig that sets the client secret.
485+
func WithClientSecret(clientSecret string) CreateOIDCKubeconfigOption {
486+
return func(opts *CreateOIDCKubeconfigOptions) {
487+
opts.ClientSecret = clientSecret
488+
}
489+
}
490+
491+
// WithContextName allows to override the default context name "cluster" in the kubeconfig.
492+
func WithContextName(contextName string) CreateOIDCKubeconfigOption {
493+
return func(opts *CreateOIDCKubeconfigOptions) {
494+
opts.ContextName = contextName
495+
if opts.ContextName == "" {
496+
opts.ContextName = "cluster"
497+
}
498+
}
499+
}
500+
501+
// WithClusterName allows to override the default cluster name "cluster" in the kubeconfig.
502+
func WithClusterName(clusterName string) CreateOIDCKubeconfigOption {
503+
return func(opts *CreateOIDCKubeconfigOptions) {
504+
opts.ClusterName = clusterName
505+
if opts.ClusterName == "" {
506+
opts.ClusterName = "cluster"
507+
}
508+
}
509+
}
510+
347511
// oidcTrustConfig represents the configuration for an OIDC trust relationship.
348512
// It includes the host of the Kubernetes API server, CA data for TLS verification,
349513
// and the audience for the OIDC tokens.

pkg/clusteraccess/access_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,4 +462,72 @@ var _ = Describe("ClusterAccess", func() {
462462
})
463463
})
464464

465+
Context("CreateOIDCKubeconfig", func() {
466+
467+
It("should create a kubeconfig with oidc-login plugin (no options)", func() {
468+
kcfgBytes, err := clusteraccess.CreateOIDCKubeconfig("testuser", "https://api.example.com", []byte("test-ca"), "https://example.com/oidc", "test-client-id")
469+
Expect(err).ToNot(HaveOccurred())
470+
Expect(kcfgBytes).ToNot(BeEmpty())
471+
472+
kcfg, err := clientcmd.Load(kcfgBytes)
473+
Expect(err).ToNot(HaveOccurred())
474+
id := "cluster"
475+
Expect(kcfg.CurrentContext).To(Equal(id))
476+
Expect(kcfg.Contexts[id].Cluster).To(Equal(id))
477+
Expect(kcfg.Contexts[id].AuthInfo).To(Equal("testuser"))
478+
Expect(kcfg.Clusters[id].Server).To(Equal("https://api.example.com"))
479+
Expect(kcfg.Clusters[id].CertificateAuthorityData).To(Equal([]byte("test-ca")))
480+
auth := kcfg.AuthInfos["testuser"]
481+
Expect(auth).ToNot(BeNil())
482+
Expect(auth.Exec).ToNot(BeNil())
483+
Expect(auth.Exec.Command).To(Equal("kubectl"))
484+
Expect(auth.Exec.Args[:2]).To(Equal([]string{"oidc-login", "get-token"}))
485+
Expect(auth.Exec.Args[2:]).To(ConsistOf(
486+
"--oidc-issuer-url=https://example.com/oidc",
487+
"--oidc-client-id=test-client-id",
488+
"--grant-type=auto",
489+
))
490+
})
491+
492+
It("should create a kubeconfig with oidc-login plugin (all options)", func() {
493+
contextId := "my-context"
494+
clusterId := "my-cluster"
495+
kcfgBytes, err := clusteraccess.CreateOIDCKubeconfig("testuser", "https://api.example.com", []byte("test-ca"), "https://example.com/oidc", "test-client-id",
496+
clusteraccess.WithExtraScope("foo"),
497+
clusteraccess.WithExtraScope("bar"),
498+
clusteraccess.UsePKCE(),
499+
clusteraccess.ForceRefresh(),
500+
clusteraccess.WithClientSecret("test-client-secret"),
501+
clusteraccess.WithGrantType(clusteraccess.GrantTypePassword),
502+
clusteraccess.WithContextName(contextId),
503+
clusteraccess.WithClusterName(clusterId))
504+
Expect(err).ToNot(HaveOccurred())
505+
Expect(kcfgBytes).ToNot(BeEmpty())
506+
507+
kcfg, err := clientcmd.Load(kcfgBytes)
508+
Expect(err).ToNot(HaveOccurred())
509+
Expect(kcfg.CurrentContext).To(Equal(contextId))
510+
Expect(kcfg.Contexts[contextId].Cluster).To(Equal(clusterId))
511+
Expect(kcfg.Contexts[contextId].AuthInfo).To(Equal("testuser"))
512+
Expect(kcfg.Clusters[clusterId].Server).To(Equal("https://api.example.com"))
513+
Expect(kcfg.Clusters[clusterId].CertificateAuthorityData).To(Equal([]byte("test-ca")))
514+
auth := kcfg.AuthInfos["testuser"]
515+
Expect(auth).ToNot(BeNil())
516+
Expect(auth.Exec).ToNot(BeNil())
517+
Expect(auth.Exec.Command).To(Equal("kubectl"))
518+
Expect(auth.Exec.Args[:2]).To(Equal([]string{"oidc-login", "get-token"}))
519+
Expect(auth.Exec.Args[2:]).To(ConsistOf(
520+
"--oidc-issuer-url=https://example.com/oidc",
521+
"--oidc-client-id=test-client-id",
522+
"--oidc-client-secret=test-client-secret",
523+
"--grant-type=password",
524+
"--oidc-extra-scope=foo",
525+
"--oidc-extra-scope=bar",
526+
"--oidc-use-pkce",
527+
"--force-refresh",
528+
))
529+
})
530+
531+
})
532+
465533
})

0 commit comments

Comments
 (0)