diff --git a/.golangci.yaml b/.golangci.yaml index ec1e153c..9578fd7e 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -60,7 +60,7 @@ issues: linters-settings: funlen: - lines: 125 + lines: 150 statements: 50 goheader: template: |- diff --git a/Dockerfile b/Dockerfile index 7ddea4d4..73803e6e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ RUN go mod download # Copy only the production source code to avoid cache misses when editing other files COPY generated ./generated COPY cmd ./cmd +COPY pkg ./pkg COPY internal ./internal COPY tools ./tools COPY hack ./hack diff --git a/apis/supervisor/idp/v1alpha1/types_tls.go.tmpl b/apis/supervisor/idp/v1alpha1/types_tls.go.tmpl new file mode 100644 index 00000000..fa4db315 --- /dev/null +++ b/apis/supervisor/idp/v1alpha1/types_tls.go.tmpl @@ -0,0 +1,11 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +// Configuration for TLS parameters related to identity provider integration. +type TLSSpec struct { + // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + // +optional + CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` +} diff --git a/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go.tmpl index cc8ca0fa..ea12b063 100644 --- a/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go.tmpl @@ -75,6 +75,10 @@ type UpstreamOIDCProviderSpec struct { // +kubebuilder:validation:Pattern=`^https://` Issuer string `json:"issuer"` + // TLS configuration for discovery/JWKS requests to the issuer. + // +optional + TLS *TLSSpec `json:"tls,omitempty"` + // AuthorizationConfig holds information about how to form the OAuth2 authorization request // parameters to be used with this OIDC identity provider. // +optional diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index 3754f06a..1677c00f 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -4,7 +4,12 @@ package cmd import ( + "crypto/tls" + "crypto/x509" "encoding/json" + "fmt" + "io/ioutil" + "net/http" "os" "path/filepath" @@ -13,8 +18,8 @@ import ( clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" "k8s.io/klog/v2/klogr" - "go.pinniped.dev/internal/oidcclient" - "go.pinniped.dev/internal/oidcclient/filesession" + "go.pinniped.dev/pkg/oidcclient" + "go.pinniped.dev/pkg/oidcclient/filesession" ) //nolint: gochecknoinits @@ -36,6 +41,7 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...oid scopes []string skipBrowser bool sessionCachePath string + caBundlePaths []string debugSessionCache bool ) cmd.Flags().StringVar(&issuer, "issuer", "", "OpenID Connect issuer URL.") @@ -44,6 +50,7 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...oid cmd.Flags().StringSliceVar(&scopes, "scopes", []string{"offline_access", "openid", "email", "profile"}, "OIDC scopes to request during login.") cmd.Flags().BoolVar(&skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL).") cmd.Flags().StringVar(&sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file.") + cmd.Flags().StringSliceVar(&caBundlePaths, "ca-bundle", nil, "Path to TLS certificate authority bundle (PEM format, optional, can be repeated).") cmd.Flags().BoolVar(&debugSessionCache, "debug-session-cache", false, "Print debug logs related to the session cache.") mustMarkHidden(&cmd, "debug-session-cache") mustMarkRequired(&cmd, "issuer", "client-id") @@ -80,6 +87,27 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...oid })) } + if len(caBundlePaths) > 0 { + pool := x509.NewCertPool() + for _, p := range caBundlePaths { + pem, err := ioutil.ReadFile(p) + if err != nil { + return fmt.Errorf("could not read --ca-bundle: %w", err) + } + pool.AppendCertsFromPEM(pem) + } + tlsConfig := tls.Config{ + RootCAs: pool, + MinVersion: tls.VersionTLS12, + } + opts = append(opts, oidcclient.WithClient(&http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tlsConfig, + }, + })) + } + tok, err := loginFunc(issuer, clientID, opts...) if err != nil { return err diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index 637a7af4..3a61934d 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -12,7 +12,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "go.pinniped.dev/internal/here" - "go.pinniped.dev/internal/oidcclient" + "go.pinniped.dev/pkg/oidcclient" ) func TestLoginOIDCCommand(t *testing.T) { @@ -40,6 +40,7 @@ func TestLoginOIDCCommand(t *testing.T) { oidc --issuer ISSUER --client-id CLIENT_ID [flags] Flags: + --ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated). --client-id string OpenID Connect client ID. -h, --help help for oidc --issuer string OpenID Connect issuer URL. diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml index 451a4474..780fe6fe 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml @@ -98,6 +98,15 @@ spec: minLength: 1 pattern: ^https:// type: string + tls: + description: TLS configuration for discovery/JWKS requests to the + issuer. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM bundle). + If omitted, a default set of system roots will be trusted. + type: string + type: object required: - client - issuer diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 1854d94a..a81aa6e9 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -373,6 +373,23 @@ OIDCClient contains information about an OIDC client (e.g., client ID and client |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-tlsspec"] +==== TLSSpec + +Configuration for TLS parameters related to identity provider integration. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-upstreamoidcproviderspec[$$UpstreamOIDCProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-upstreamoidcprovider"] ==== UpstreamOIDCProvider @@ -409,6 +426,7 @@ Spec for configuring an OIDC identity provider. |=== | Field | Description | *`issuer`* __string__ | Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch /.well-known/openid-configuration. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for discovery/JWKS requests to the issuer. | *`authorizationConfig`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]__ | AuthorizationConfig holds information about how to form the OAuth2 authorization request parameters to be used with this OIDC identity provider. | *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcclaims[$$OIDCClaims$$]__ | Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. | *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcclient[$$OIDCClient$$]__ | OIDCClient contains OIDC client information to be used used with this OIDC identity provider. diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_tls.go new file mode 100644 index 00000000..fa4db315 --- /dev/null +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_tls.go @@ -0,0 +1,11 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +// Configuration for TLS parameters related to identity provider integration. +type TLSSpec struct { + // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + // +optional + CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` +} diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go index cc8ca0fa..ea12b063 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go @@ -75,6 +75,10 @@ type UpstreamOIDCProviderSpec struct { // +kubebuilder:validation:Pattern=`^https://` Issuer string `json:"issuer"` + // TLS configuration for discovery/JWKS requests to the issuer. + // +optional + TLS *TLSSpec `json:"tls,omitempty"` + // AuthorizationConfig holds information about how to form the OAuth2 authorization request // parameters to be used with this OIDC identity provider. // +optional diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 07cbb8b6..9eeade4e 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -81,6 +81,22 @@ func (in *OIDCClient) DeepCopy() *OIDCClient { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSpec. +func (in *TLSSpec) DeepCopy() *TLSSpec { + if in == nil { + return nil + } + out := new(TLSSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) { *out = *in @@ -145,6 +161,11 @@ func (in *UpstreamOIDCProviderList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) { *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) out.Claims = in.Claims out.Client = in.Client diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml index 451a4474..780fe6fe 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml @@ -98,6 +98,15 @@ spec: minLength: 1 pattern: ^https:// type: string + tls: + description: TLS configuration for discovery/JWKS requests to the + issuer. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM bundle). + If omitted, a default set of system roots will be trusted. + type: string + type: object required: - client - issuer diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index bb11a577..a2a20313 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -373,6 +373,23 @@ OIDCClient contains information about an OIDC client (e.g., client ID and client |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-tlsspec"] +==== TLSSpec + +Configuration for TLS parameters related to identity provider integration. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-upstreamoidcproviderspec[$$UpstreamOIDCProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-upstreamoidcprovider"] ==== UpstreamOIDCProvider @@ -409,6 +426,7 @@ Spec for configuring an OIDC identity provider. |=== | Field | Description | *`issuer`* __string__ | Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch /.well-known/openid-configuration. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for discovery/JWKS requests to the issuer. | *`authorizationConfig`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]__ | AuthorizationConfig holds information about how to form the OAuth2 authorization request parameters to be used with this OIDC identity provider. | *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcclaims[$$OIDCClaims$$]__ | Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. | *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcclient[$$OIDCClient$$]__ | OIDCClient contains OIDC client information to be used used with this OIDC identity provider. diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_tls.go new file mode 100644 index 00000000..fa4db315 --- /dev/null +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_tls.go @@ -0,0 +1,11 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +// Configuration for TLS parameters related to identity provider integration. +type TLSSpec struct { + // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + // +optional + CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` +} diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go index cc8ca0fa..ea12b063 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go @@ -75,6 +75,10 @@ type UpstreamOIDCProviderSpec struct { // +kubebuilder:validation:Pattern=`^https://` Issuer string `json:"issuer"` + // TLS configuration for discovery/JWKS requests to the issuer. + // +optional + TLS *TLSSpec `json:"tls,omitempty"` + // AuthorizationConfig holds information about how to form the OAuth2 authorization request // parameters to be used with this OIDC identity provider. // +optional diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 07cbb8b6..9eeade4e 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -81,6 +81,22 @@ func (in *OIDCClient) DeepCopy() *OIDCClient { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSpec. +func (in *TLSSpec) DeepCopy() *TLSSpec { + if in == nil { + return nil + } + out := new(TLSSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) { *out = *in @@ -145,6 +161,11 @@ func (in *UpstreamOIDCProviderList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) { *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) out.Claims = in.Claims out.Client = in.Client diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml index 451a4474..780fe6fe 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml @@ -98,6 +98,15 @@ spec: minLength: 1 pattern: ^https:// type: string + tls: + description: TLS configuration for discovery/JWKS requests to the + issuer. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM bundle). + If omitted, a default set of system roots will be trusted. + type: string + type: object required: - client - issuer diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index 500bf9ea..e36be991 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -373,6 +373,23 @@ OIDCClient contains information about an OIDC client (e.g., client ID and client |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-tlsspec"] +==== TLSSpec + +Configuration for TLS parameters related to identity provider integration. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-upstreamoidcproviderspec[$$UpstreamOIDCProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-upstreamoidcprovider"] ==== UpstreamOIDCProvider @@ -409,6 +426,7 @@ Spec for configuring an OIDC identity provider. |=== | Field | Description | *`issuer`* __string__ | Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch /.well-known/openid-configuration. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for discovery/JWKS requests to the issuer. | *`authorizationConfig`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]__ | AuthorizationConfig holds information about how to form the OAuth2 authorization request parameters to be used with this OIDC identity provider. | *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcclaims[$$OIDCClaims$$]__ | Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. | *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcclient[$$OIDCClient$$]__ | OIDCClient contains OIDC client information to be used used with this OIDC identity provider. diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_tls.go new file mode 100644 index 00000000..fa4db315 --- /dev/null +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_tls.go @@ -0,0 +1,11 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +// Configuration for TLS parameters related to identity provider integration. +type TLSSpec struct { + // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + // +optional + CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` +} diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go index cc8ca0fa..ea12b063 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go @@ -75,6 +75,10 @@ type UpstreamOIDCProviderSpec struct { // +kubebuilder:validation:Pattern=`^https://` Issuer string `json:"issuer"` + // TLS configuration for discovery/JWKS requests to the issuer. + // +optional + TLS *TLSSpec `json:"tls,omitempty"` + // AuthorizationConfig holds information about how to form the OAuth2 authorization request // parameters to be used with this OIDC identity provider. // +optional diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 07cbb8b6..9eeade4e 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -81,6 +81,22 @@ func (in *OIDCClient) DeepCopy() *OIDCClient { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSpec. +func (in *TLSSpec) DeepCopy() *TLSSpec { + if in == nil { + return nil + } + out := new(TLSSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) { *out = *in @@ -145,6 +161,11 @@ func (in *UpstreamOIDCProviderList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) { *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) out.Claims = in.Claims out.Client = in.Client diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml index 451a4474..780fe6fe 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml @@ -98,6 +98,15 @@ spec: minLength: 1 pattern: ^https:// type: string + tls: + description: TLS configuration for discovery/JWKS requests to the + issuer. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM bundle). + If omitted, a default set of system roots will be trusted. + type: string + type: object required: - client - issuer diff --git a/hack/lib/tilt/Tiltfile b/hack/lib/tilt/Tiltfile index cc4d44d5..e657e967 100644 --- a/hack/lib/tilt/Tiltfile +++ b/hack/lib/tilt/Tiltfile @@ -19,23 +19,25 @@ local_resource( ) ##################################################################################################### -# Dex +# Test IDP (Dex + cert generation + squid proxy) # -# Render the Dex installation manifest using ytt. +# Render the IDP installation manifest using ytt. k8s_yaml(local(['ytt','--file', '../../../test/deploy/dex'])) # Tell tilt to watch all of those files for changes. watch_file('../../../test/deploy/dex') -# Collect all the deployed Dex resources under a "dex" resource tab. -k8s_resource( - workload='dex', # this is the deployment name - objects=[ - # these are the objects that would otherwise appear in the "uncategorized" tab in the tilt UI - 'dex:namespace', - 'dex-config:configmap', - ], -) +k8s_resource(objects=['dex:namespace'], new_name='dex-ns') +k8s_resource(workload='cert-issuer', resource_deps=['dex-ns'], objects=[ + 'cert-issuer:serviceaccount', + 'cert-issuer:role', + 'cert-issuer:rolebinding', +]) +k8s_resource(workload='proxy', resource_deps=['dex-ns']) +k8s_resource(workload='dex', resource_deps=['dex-ns', 'cert-issuer'], objects=[ + 'dex-config:configmap', +]) + ##################################################################################################### # Local-user-authenticator app @@ -186,6 +188,6 @@ k8s_resource( local_resource( 'test-env', 'TILT_MODE=yes ../../prepare-for-integration-tests.sh', - resource_deps=['local-user-auth', 'concierge', 'supervisor'], + resource_deps=['local-user-auth', 'concierge', 'supervisor', 'dex', 'proxy'], deps=['../../prepare-for-integration-tests.sh'], ) diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index d3d7cb24..09155dbb 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -265,6 +265,11 @@ if ! tilt_mode; then popd >/dev/null fi +# +# Download the test CA bundle that was generated in the Dex pod. +# +test_ca_bundle_pem="$(kubectl get secrets -n dex certs -o go-template='{{index .data "ca.pem" | base64decode}}')" + # # Create the environment file # @@ -286,7 +291,9 @@ export PINNIPED_TEST_SUPERVISOR_APP_NAME=${supervisor_app_name} export PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS='${supervisor_custom_labels}' export PINNIPED_TEST_SUPERVISOR_HTTP_ADDRESS="127.0.0.1:12345" export PINNIPED_TEST_SUPERVISOR_HTTPS_ADDRESS="localhost:12344" -export PINNIPED_TEST_CLI_OIDC_ISSUER=http://127.0.0.1:12346/dex +export PINNIPED_TEST_PROXY=http://127.0.0.1:12346 +export PINNIPED_TEST_CLI_OIDC_ISSUER=https://dex.dex.svc.cluster.local/dex +export PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE="${test_ca_bundle_pem}" export PINNIPED_TEST_CLI_OIDC_CLIENT_ID=pinniped-cli export PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT=48095 export PINNIPED_TEST_CLI_OIDC_USERNAME=pinny@example.com diff --git a/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher.go b/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher.go index 144d2944..bc3db3bf 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher.go @@ -6,7 +6,11 @@ package upstreamwatcher import ( "context" + "crypto/tls" + "crypto/x509" + "encoding/base64" "fmt" + "net/http" "net/url" "sort" "time" @@ -48,10 +52,12 @@ const ( reasonMissingKeys = "SecretMissingKeys" reasonSuccess = "Success" reasonUnreachable = "Unreachable" + reasonInvalidTLSConfig = "InvalidTLSConfig" reasonInvalidResponse = "InvalidResponse" // Errors that are generated by our reconcile process. - errFailureStatus = constable.Error("UpstreamOIDCProvider has a failing condition") + errFailureStatus = constable.Error("UpstreamOIDCProvider has a failing condition") + errNoCertificates = constable.Error("no certificates found") ) // IDPCache is a thread safe cache that holds a list of validated upstream OIDC IDP configurations. @@ -59,13 +65,39 @@ type IDPCache interface { SetIDPList([]provider.UpstreamOIDCIdentityProvider) } +// lruValidatorCache caches the *oidc.Provider associated with a particular issuer/TLS configuration. +type lruValidatorCache struct{ cache *cache.Expiring } + +func (c *lruValidatorCache) getProvider(spec *v1alpha1.UpstreamOIDCProviderSpec) *oidc.Provider { + if result, ok := c.cache.Get(c.cacheKey(spec)); ok { + return result.(*oidc.Provider) + } + return nil +} + +func (c *lruValidatorCache) putProvider(spec *v1alpha1.UpstreamOIDCProviderSpec, provider *oidc.Provider) { + c.cache.Set(c.cacheKey(spec), provider, validatorCacheTTL) +} + +func (c *lruValidatorCache) cacheKey(spec *v1alpha1.UpstreamOIDCProviderSpec) interface{} { + var key struct{ issuer, caBundle string } + key.issuer = spec.Issuer + if spec.TLS != nil { + key.caBundle = spec.TLS.CertificateAuthorityData + } + return key +} + type controller struct { cache IDPCache log logr.Logger client pinnipedclientset.Interface providers idpinformers.UpstreamOIDCProviderInformer secrets corev1informers.SecretInformer - validatorCache *cache.Expiring + validatorCache interface { + getProvider(spec *v1alpha1.UpstreamOIDCProviderSpec) *oidc.Provider + putProvider(spec *v1alpha1.UpstreamOIDCProviderSpec, provider *oidc.Provider) + } } // New instantiates a new controllerlib.Controller which will populate the provided IDPCache. @@ -82,7 +114,7 @@ func New( client: client, providers: providers, secrets: secrets, - validatorCache: cache.NewExpiring(), + validatorCache: &lruValidatorCache{cache: cache.NewExpiring()}, } filter := pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()) return controllerlib.New( @@ -197,15 +229,22 @@ func (c *controller) validateSecret(upstream *v1alpha1.UpstreamOIDCProvider, res // validateIssuer validates the .spec.issuer field, performs OIDC discovery, and returns the appropriate OIDCDiscoverySucceeded condition. func (c *controller) validateIssuer(ctx context.Context, upstream *v1alpha1.UpstreamOIDCProvider, result *provider.UpstreamOIDCIdentityProvider) *v1alpha1.Condition { // Get the provider (from cache if possible). - var discoveredProvider *oidc.Provider - if cached, ok := c.validatorCache.Get(upstream.Spec.Issuer); ok { - discoveredProvider = cached.(*oidc.Provider) - } + discoveredProvider := c.validatorCache.getProvider(&upstream.Spec) // If the provider does not exist in the cache, do a fresh discovery lookup and save to the cache. if discoveredProvider == nil { - var err error - discoveredProvider, err = oidc.NewProvider(ctx, upstream.Spec.Issuer) + tlsConfig, err := getTLSConfig(upstream) + if err != nil { + return &v1alpha1.Condition{ + Type: typeOIDCDiscoverySucceeded, + Status: v1alpha1.ConditionFalse, + Reason: reasonInvalidTLSConfig, + Message: err.Error(), + } + } + httpClient := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}} + + discoveredProvider, err = oidc.NewProvider(oidc.ClientContext(ctx, httpClient), upstream.Spec.Issuer) if err != nil { return &v1alpha1.Condition{ Type: typeOIDCDiscoverySucceeded, @@ -216,7 +255,7 @@ func (c *controller) validateIssuer(ctx context.Context, upstream *v1alpha1.Upst } // Update the cache with the newly discovered value. - c.validatorCache.Set(upstream.Spec.Issuer, discoveredProvider, validatorCacheTTL) + c.validatorCache.putProvider(&upstream.Spec, discoveredProvider) } // Parse out and validate the discovered authorize endpoint. @@ -248,6 +287,28 @@ func (c *controller) validateIssuer(ctx context.Context, upstream *v1alpha1.Upst } } +func getTLSConfig(upstream *v1alpha1.UpstreamOIDCProvider) (*tls.Config, error) { + result := tls.Config{ + MinVersion: tls.VersionTLS12, + } + + if upstream.Spec.TLS == nil || upstream.Spec.TLS.CertificateAuthorityData == "" { + return &result, nil + } + + bundle, err := base64.StdEncoding.DecodeString(upstream.Spec.TLS.CertificateAuthorityData) + if err != nil { + return nil, fmt.Errorf("spec.certificateAuthorityData is invalid: %w", err) + } + + result.RootCAs = x509.NewCertPool() + if !result.RootCAs.AppendCertsFromPEM(bundle) { + return nil, fmt.Errorf("spec.certificateAuthorityData is invalid: %w", errNoCertificates) + } + + return &result, nil +} + func (c *controller) updateStatus(ctx context.Context, upstream *v1alpha1.UpstreamOIDCProvider, conditions []*v1alpha1.Condition) { log := c.log.WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() diff --git a/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go index 2e91a523..949effaf 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go @@ -5,9 +5,9 @@ package upstreamwatcher import ( "context" + "encoding/base64" "encoding/json" "net/http" - "net/http/httptest" "net/url" "strings" "testing" @@ -25,6 +25,7 @@ import ( pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/testlogger" ) @@ -34,7 +35,8 @@ func TestController(t *testing.T) { earlier := metav1.NewTime(now.Add(-1 * time.Hour).UTC()) // Start another test server that answers discovery successfully. - testIssuer := newTestIssuer(t) + testIssuerCA, testIssuerURL := newTestIssuer(t) + testIssuerCABase64 := base64.StdEncoding.EncodeToString([]byte(testIssuerCA)) testIssuerAuthorizeURL, err := url.Parse("https://example.com/authorize") require.NoError(t, err) @@ -65,7 +67,8 @@ func TestController(t *testing.T) { inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.UpstreamOIDCProviderSpec{ - Issuer: testIssuer.URL, + Issuer: testIssuerURL, + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, Client: v1alpha1.OIDCClient{SecretName: testSecretName}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, }, @@ -106,7 +109,8 @@ func TestController(t *testing.T) { inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.UpstreamOIDCProviderSpec{ - Issuer: testIssuer.URL, + Issuer: testIssuerURL, + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, Client: v1alpha1.OIDCClient{SecretName: testSecretName}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, }, @@ -151,7 +155,8 @@ func TestController(t *testing.T) { inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.UpstreamOIDCProviderSpec{ - Issuer: testIssuer.URL, + Issuer: testIssuerURL, + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, Client: v1alpha1.OIDCClient{SecretName: testSecretName}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, }, @@ -190,6 +195,102 @@ func TestController(t *testing.T) { }, }}, }, + { + name: "TLS CA bundle is invalid base64", + inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name"}, + Spec: v1alpha1.UpstreamOIDCProviderSpec{ + Issuer: testIssuerURL, + TLS: &v1alpha1.TLSSpec{ + CertificateAuthorityData: "invalid-base64", + }, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, + AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")}, + }, + }}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, + Type: "secrets.pinniped.dev/oidc-client", + Data: testValidSecretData, + }}, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantLogs: []string{ + `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `upstream-observer "error"="UpstreamOIDCProvider has a failing condition" "msg"="found failing condition" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`, + }, + wantResultingCache: []provider.UpstreamOIDCIdentityProvider{}, + wantResultingUpstreams: []v1alpha1.UpstreamOIDCProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, + Status: v1alpha1.UpstreamOIDCProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + { + Type: "ClientCredentialsValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded client credentials", + }, + { + Type: "OIDCDiscoverySucceeded", + Status: "False", + LastTransitionTime: now, + Reason: "InvalidTLSConfig", + Message: `spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7`, + }, + }, + }, + }}, + }, + { + name: "TLS CA bundle does not have any certificates", + inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name"}, + Spec: v1alpha1.UpstreamOIDCProviderSpec{ + Issuer: testIssuerURL, + TLS: &v1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("not-a-pem-ca-bundle")), + }, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, + AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")}, + }, + }}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, + Type: "secrets.pinniped.dev/oidc-client", + Data: testValidSecretData, + }}, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantLogs: []string{ + `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: no certificates found" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `upstream-observer "error"="UpstreamOIDCProvider has a failing condition" "msg"="found failing condition" "message"="spec.certificateAuthorityData is invalid: no certificates found" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`, + }, + wantResultingCache: []provider.UpstreamOIDCIdentityProvider{}, + wantResultingUpstreams: []v1alpha1.UpstreamOIDCProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, + Status: v1alpha1.UpstreamOIDCProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + { + Type: "ClientCredentialsValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded client credentials", + }, + { + Type: "OIDCDiscoverySucceeded", + Status: "False", + LastTransitionTime: now, + Reason: "InvalidTLSConfig", + Message: `spec.certificateAuthorityData is invalid: no certificates found`, + }, + }, + }, + }}, + }, { name: "issuer is invalid URL", inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ @@ -240,7 +341,8 @@ func TestController(t *testing.T) { inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.UpstreamOIDCProviderSpec{ - Issuer: testIssuer.URL + "/invalid", + Issuer: testIssuerURL + "/invalid", + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, Client: v1alpha1.OIDCClient{SecretName: testSecretName}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, }, @@ -285,7 +387,8 @@ func TestController(t *testing.T) { inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.UpstreamOIDCProviderSpec{ - Issuer: testIssuer.URL + "/insecure", + Issuer: testIssuerURL + "/insecure", + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, Client: v1alpha1.OIDCClient{SecretName: testSecretName}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, }, @@ -330,7 +433,8 @@ func TestController(t *testing.T) { inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name"}, Spec: v1alpha1.UpstreamOIDCProviderSpec{ - Issuer: testIssuer.URL, + Issuer: testIssuerURL, + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, Client: v1alpha1.OIDCClient{SecretName: testSecretName}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")}, }, @@ -373,7 +477,8 @@ func TestController(t *testing.T) { inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Spec: v1alpha1.UpstreamOIDCProviderSpec{ - Issuer: testIssuer.URL, + Issuer: testIssuerURL, + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, Client: v1alpha1.OIDCClient{SecretName: testSecretName}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, }, @@ -486,10 +591,9 @@ func normalizeUpstreams(upstreams []v1alpha1.UpstreamOIDCProvider, now metav1.Ti return result } -func newTestIssuer(t *testing.T) *httptest.Server { +func newTestIssuer(t *testing.T) (string, string) { mux := http.NewServeMux() - testServer := httptest.NewServer(mux) - t.Cleanup(testServer.Close) + caBundlePEM, testURL := testutil.TLSTestServer(t, mux.ServeHTTP) type providerJSON struct { Issuer string `json:"issuer"` @@ -502,7 +606,7 @@ func newTestIssuer(t *testing.T) *httptest.Server { mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", "application/json") _ = json.NewEncoder(w).Encode(&providerJSON{ - Issuer: testServer.URL, + Issuer: testURL, AuthURL: "https://example.com/authorize", }) }) @@ -511,7 +615,7 @@ func newTestIssuer(t *testing.T) *httptest.Server { mux.HandleFunc("/invalid/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", "application/json") _ = json.NewEncoder(w).Encode(&providerJSON{ - Issuer: testServer.URL + "/invalid", + Issuer: testURL + "/invalid", AuthURL: "%", }) }) @@ -520,10 +624,10 @@ func newTestIssuer(t *testing.T) *httptest.Server { mux.HandleFunc("/insecure/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", "application/json") _ = json.NewEncoder(w).Encode(&providerJSON{ - Issuer: testServer.URL + "/insecure", + Issuer: testURL + "/insecure", AuthURL: "http://example.com/authorize", }) }) - return testServer + return caBundlePEM, testURL } diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index de3c7f71..ec752cf3 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -19,9 +19,9 @@ import ( "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/oidcclient/nonce" - "go.pinniped.dev/internal/oidcclient/pkce" "go.pinniped.dev/internal/plog" + "go.pinniped.dev/pkg/oidcclient/nonce" + "go.pinniped.dev/pkg/oidcclient/pkce" ) const ( diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index e0fdbacd..dcd2f9d6 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -21,8 +21,8 @@ import ( "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/oidcclient/nonce" - "go.pinniped.dev/internal/oidcclient/pkce" + "go.pinniped.dev/pkg/oidcclient/nonce" + "go.pinniped.dev/pkg/oidcclient/pkce" ) func TestAuthorizationEndpoint(t *testing.T) { diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index 0687ba22..723cfe67 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -16,9 +16,9 @@ import ( "go.pinniped.dev/internal/oidc/discovery" "go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/oidcclient/nonce" - "go.pinniped.dev/internal/oidcclient/pkce" "go.pinniped.dev/internal/plog" + "go.pinniped.dev/pkg/oidcclient/nonce" + "go.pinniped.dev/pkg/oidcclient/pkce" ) // Manager can manage multiple active OIDC providers. It acts as a request router for them. diff --git a/internal/oidcclient/filesession/cachefile.go b/pkg/oidcclient/filesession/cachefile.go similarity index 99% rename from internal/oidcclient/filesession/cachefile.go rename to pkg/oidcclient/filesession/cachefile.go index 14c055f3..3629ca5f 100644 --- a/internal/oidcclient/filesession/cachefile.go +++ b/pkg/oidcclient/filesession/cachefile.go @@ -16,7 +16,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" - "go.pinniped.dev/internal/oidcclient" + "go.pinniped.dev/pkg/oidcclient" ) var ( diff --git a/internal/oidcclient/filesession/cachefile_test.go b/pkg/oidcclient/filesession/cachefile_test.go similarity index 99% rename from internal/oidcclient/filesession/cachefile_test.go rename to pkg/oidcclient/filesession/cachefile_test.go index d9094271..0ddcdf9b 100644 --- a/internal/oidcclient/filesession/cachefile_test.go +++ b/pkg/oidcclient/filesession/cachefile_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "go.pinniped.dev/internal/oidcclient" + "go.pinniped.dev/pkg/oidcclient" ) // validSession should be the same data as `testdata/valid.yaml`. diff --git a/internal/oidcclient/filesession/filesession.go b/pkg/oidcclient/filesession/filesession.go similarity index 99% rename from internal/oidcclient/filesession/filesession.go rename to pkg/oidcclient/filesession/filesession.go index 880d608e..47e0f761 100644 --- a/internal/oidcclient/filesession/filesession.go +++ b/pkg/oidcclient/filesession/filesession.go @@ -15,7 +15,7 @@ import ( "github.com/gofrs/flock" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "go.pinniped.dev/internal/oidcclient" + "go.pinniped.dev/pkg/oidcclient" ) const ( diff --git a/internal/oidcclient/filesession/filesession_test.go b/pkg/oidcclient/filesession/filesession_test.go similarity index 99% rename from internal/oidcclient/filesession/filesession_test.go rename to pkg/oidcclient/filesession/filesession_test.go index 8dd07bc9..1b28e184 100644 --- a/internal/oidcclient/filesession/filesession_test.go +++ b/pkg/oidcclient/filesession/filesession_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "go.pinniped.dev/internal/oidcclient" + "go.pinniped.dev/pkg/oidcclient" ) func TestNew(t *testing.T) { diff --git a/internal/oidcclient/filesession/testdata/invalid.yaml b/pkg/oidcclient/filesession/testdata/invalid.yaml similarity index 100% rename from internal/oidcclient/filesession/testdata/invalid.yaml rename to pkg/oidcclient/filesession/testdata/invalid.yaml diff --git a/internal/oidcclient/filesession/testdata/valid.yaml b/pkg/oidcclient/filesession/testdata/valid.yaml similarity index 100% rename from internal/oidcclient/filesession/testdata/valid.yaml rename to pkg/oidcclient/filesession/testdata/valid.yaml diff --git a/internal/oidcclient/filesession/testdata/wrong-version.yaml b/pkg/oidcclient/filesession/testdata/wrong-version.yaml similarity index 100% rename from internal/oidcclient/filesession/testdata/wrong-version.yaml rename to pkg/oidcclient/filesession/testdata/wrong-version.yaml diff --git a/internal/oidcclient/login.go b/pkg/oidcclient/login.go similarity index 96% rename from internal/oidcclient/login.go rename to pkg/oidcclient/login.go index 5ab3b5ff..0898f944 100644 --- a/internal/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -20,9 +20,9 @@ import ( "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/securityheader" - "go.pinniped.dev/internal/oidcclient/nonce" - "go.pinniped.dev/internal/oidcclient/pkce" - "go.pinniped.dev/internal/oidcclient/state" + "go.pinniped.dev/pkg/oidcclient/nonce" + "go.pinniped.dev/pkg/oidcclient/pkce" + "go.pinniped.dev/pkg/oidcclient/state" ) const ( @@ -44,6 +44,8 @@ type handlerState struct { scopes []string cache SessionCache + httpClient *http.Client + // Parameters of the localhost listener. listenAddr string callbackPath string @@ -122,6 +124,14 @@ func WithSessionCache(cache SessionCache) Option { } } +// WithClient sets the HTTP client used to make CLI-to-provider requests. +func WithClient(httpClient *http.Client) Option { + return func(h *handlerState) error { + h.httpClient = httpClient + return nil + } +} + // nopCache is a SessionCache that doesn't actually do anything. type nopCache struct{} @@ -144,6 +154,7 @@ func Login(issuer string, clientID string, opts ...Option) (*Token, error) { callbackPath: "/callback", ctx: context.Background(), callbacks: make(chan callbackResult), + httpClient: http.DefaultClient, // Default implementations of external dependencies (to be mocked in tests). generateState: state.Generate, @@ -163,6 +174,7 @@ func Login(issuer string, clientID string, opts ...Option) (*Token, error) { // Always set a long, but non-infinite timeout for this operation. ctx, cancel := context.WithTimeout(h.ctx, 10*time.Minute) defer cancel() + ctx = oidc.ClientContext(ctx, h.httpClient) h.ctx = ctx // Initialize login parameters. diff --git a/internal/oidcclient/login_test.go b/pkg/oidcclient/login_test.go similarity index 99% rename from internal/oidcclient/login_test.go rename to pkg/oidcclient/login_test.go index ea12737f..b323f586 100644 --- a/internal/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -23,9 +23,9 @@ import ( "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/mocks/mockkeyset" - "go.pinniped.dev/internal/oidcclient/nonce" - "go.pinniped.dev/internal/oidcclient/pkce" - "go.pinniped.dev/internal/oidcclient/state" + "go.pinniped.dev/pkg/oidcclient/nonce" + "go.pinniped.dev/pkg/oidcclient/pkce" + "go.pinniped.dev/pkg/oidcclient/state" ) // mockSessionCache exists to avoid an import cycle if we generate mocks into another package. @@ -416,6 +416,7 @@ func TestLogin(t *testing.T) { require.Equal(t, []*Token{&testToken}, cache.sawPutTokens) }) require.NoError(t, WithSessionCache(cache)(h)) + require.NoError(t, WithClient(&http.Client{Timeout: 10 * time.Second})(h)) h.openURL = func(actualURL string) error { parsedActualURL, err := url.Parse(actualURL) diff --git a/internal/oidcclient/nonce/nonce.go b/pkg/oidcclient/nonce/nonce.go similarity index 100% rename from internal/oidcclient/nonce/nonce.go rename to pkg/oidcclient/nonce/nonce.go diff --git a/internal/oidcclient/nonce/nonce_test.go b/pkg/oidcclient/nonce/nonce_test.go similarity index 100% rename from internal/oidcclient/nonce/nonce_test.go rename to pkg/oidcclient/nonce/nonce_test.go diff --git a/internal/oidcclient/pkce/pkce.go b/pkg/oidcclient/pkce/pkce.go similarity index 100% rename from internal/oidcclient/pkce/pkce.go rename to pkg/oidcclient/pkce/pkce.go diff --git a/internal/oidcclient/pkce/pkce_test.go b/pkg/oidcclient/pkce/pkce_test.go similarity index 100% rename from internal/oidcclient/pkce/pkce_test.go rename to pkg/oidcclient/pkce/pkce_test.go diff --git a/internal/oidcclient/state/state.go b/pkg/oidcclient/state/state.go similarity index 100% rename from internal/oidcclient/state/state.go rename to pkg/oidcclient/state/state.go diff --git a/internal/oidcclient/state/state_test.go b/pkg/oidcclient/state/state_test.go similarity index 100% rename from internal/oidcclient/state/state_test.go rename to pkg/oidcclient/state/state_test.go diff --git a/internal/oidcclient/types.go b/pkg/oidcclient/types.go similarity index 100% rename from internal/oidcclient/types.go rename to pkg/oidcclient/types.go diff --git a/site/themes/pinniped/layouts/partials/use-cases.html b/site/themes/pinniped/layouts/partials/use-cases.html index 5e0e0755..740956b3 100644 --- a/site/themes/pinniped/layouts/partials/use-cases.html +++ b/site/themes/pinniped/layouts/partials/use-cases.html @@ -12,8 +12,8 @@
Cluster Administration
-Easily plug in external IDPs into Kubernetes clusters while offering a simple install and configuration experience. Leverage first class integration with Kubernetes and kubectl CLI.
+Seamless Authentication
+Give users a consistent, unified login experience across all your clusters, including on-premises and managed cloud environments.
Cluster Administration
-Easily plug in external IDPs into Kubernetes clusters while offering a simple install and configuration experience. Leverage first class integration with Kubernetes and kubectl CLI.
+Security for Enterprises & Teams Alike
+Securely integrate with an enterprise IDP using standard protocols or use secure, externally-managed identities instead of relying on simple, shared credentials.