Merge pull request #221 from mattmoyer/use-https-dex

Add support for custom CA bundle in CLI and UpstreamOIDCProvider.
This commit is contained in:
Matt Moyer 2020-11-16 20:47:16 -06:00 committed by GitHub
commit b75a6cdb76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 529 additions and 49 deletions

View File

@ -60,7 +60,7 @@ issues:
linters-settings: linters-settings:
funlen: funlen:
lines: 125 lines: 150
statements: 50 statements: 50
goheader: goheader:
template: |- template: |-

View File

@ -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"`
}

View File

@ -75,6 +75,10 @@ type UpstreamOIDCProviderSpec struct {
// +kubebuilder:validation:Pattern=`^https://` // +kubebuilder:validation:Pattern=`^https://`
Issuer string `json:"issuer"` 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 // AuthorizationConfig holds information about how to form the OAuth2 authorization request
// parameters to be used with this OIDC identity provider. // parameters to be used with this OIDC identity provider.
// +optional // +optional

View File

@ -4,7 +4,12 @@
package cmd package cmd
import ( import (
"crypto/tls"
"crypto/x509"
"encoding/json" "encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -36,6 +41,7 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...oid
scopes []string scopes []string
skipBrowser bool skipBrowser bool
sessionCachePath string sessionCachePath string
caBundlePaths []string
debugSessionCache bool debugSessionCache bool
) )
cmd.Flags().StringVar(&issuer, "issuer", "", "OpenID Connect issuer URL.") 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().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().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().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.") cmd.Flags().BoolVar(&debugSessionCache, "debug-session-cache", false, "Print debug logs related to the session cache.")
mustMarkHidden(&cmd, "debug-session-cache") mustMarkHidden(&cmd, "debug-session-cache")
mustMarkRequired(&cmd, "issuer", "client-id") 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...) tok, err := loginFunc(issuer, clientID, opts...)
if err != nil { if err != nil {
return err return err

View File

@ -40,6 +40,7 @@ func TestLoginOIDCCommand(t *testing.T) {
oidc --issuer ISSUER --client-id CLIENT_ID [flags] oidc --issuer ISSUER --client-id CLIENT_ID [flags]
Flags: Flags:
--ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated).
--client-id string OpenID Connect client ID. --client-id string OpenID Connect client ID.
-h, --help help for oidc -h, --help help for oidc
--issuer string OpenID Connect issuer URL. --issuer string OpenID Connect issuer URL.

View File

@ -98,6 +98,15 @@ spec:
minLength: 1 minLength: 1
pattern: ^https:// pattern: ^https://
type: string 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: required:
- client - client
- issuer - issuer

View File

@ -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"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-upstreamoidcprovider"]
==== UpstreamOIDCProvider ==== UpstreamOIDCProvider
@ -409,6 +426,7 @@ Spec for configuring an OIDC identity provider.
|=== |===
| Field | Description | Field | Description
| *`issuer`* __string__ | Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch /.well-known/openid-configuration. | *`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. | *`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. | *`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. | *`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.

View File

@ -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"`
}

View File

@ -75,6 +75,10 @@ type UpstreamOIDCProviderSpec struct {
// +kubebuilder:validation:Pattern=`^https://` // +kubebuilder:validation:Pattern=`^https://`
Issuer string `json:"issuer"` 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 // AuthorizationConfig holds information about how to form the OAuth2 authorization request
// parameters to be used with this OIDC identity provider. // parameters to be used with this OIDC identity provider.
// +optional // +optional

View File

@ -81,6 +81,22 @@ func (in *OIDCClient) DeepCopy() *OIDCClient {
return out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) { func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) {
*out = *in *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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) { func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) {
*out = *in *out = *in
if in.TLS != nil {
in, out := &in.TLS, &out.TLS
*out = new(TLSSpec)
**out = **in
}
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
out.Claims = in.Claims out.Claims = in.Claims
out.Client = in.Client out.Client = in.Client

View File

@ -98,6 +98,15 @@ spec:
minLength: 1 minLength: 1
pattern: ^https:// pattern: ^https://
type: string 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: required:
- client - client
- issuer - issuer

View File

@ -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"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-upstreamoidcprovider"]
==== UpstreamOIDCProvider ==== UpstreamOIDCProvider
@ -409,6 +426,7 @@ Spec for configuring an OIDC identity provider.
|=== |===
| Field | Description | Field | Description
| *`issuer`* __string__ | Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch /.well-known/openid-configuration. | *`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. | *`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. | *`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. | *`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.

View File

@ -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"`
}

View File

@ -75,6 +75,10 @@ type UpstreamOIDCProviderSpec struct {
// +kubebuilder:validation:Pattern=`^https://` // +kubebuilder:validation:Pattern=`^https://`
Issuer string `json:"issuer"` 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 // AuthorizationConfig holds information about how to form the OAuth2 authorization request
// parameters to be used with this OIDC identity provider. // parameters to be used with this OIDC identity provider.
// +optional // +optional

View File

@ -81,6 +81,22 @@ func (in *OIDCClient) DeepCopy() *OIDCClient {
return out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) { func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) {
*out = *in *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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) { func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) {
*out = *in *out = *in
if in.TLS != nil {
in, out := &in.TLS, &out.TLS
*out = new(TLSSpec)
**out = **in
}
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
out.Claims = in.Claims out.Claims = in.Claims
out.Client = in.Client out.Client = in.Client

View File

@ -98,6 +98,15 @@ spec:
minLength: 1 minLength: 1
pattern: ^https:// pattern: ^https://
type: string 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: required:
- client - client
- issuer - issuer

View File

@ -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"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-upstreamoidcprovider"]
==== UpstreamOIDCProvider ==== UpstreamOIDCProvider
@ -409,6 +426,7 @@ Spec for configuring an OIDC identity provider.
|=== |===
| Field | Description | Field | Description
| *`issuer`* __string__ | Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch /.well-known/openid-configuration. | *`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. | *`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. | *`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. | *`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.

View File

@ -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"`
}

View File

@ -75,6 +75,10 @@ type UpstreamOIDCProviderSpec struct {
// +kubebuilder:validation:Pattern=`^https://` // +kubebuilder:validation:Pattern=`^https://`
Issuer string `json:"issuer"` 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 // AuthorizationConfig holds information about how to form the OAuth2 authorization request
// parameters to be used with this OIDC identity provider. // parameters to be used with this OIDC identity provider.
// +optional // +optional

View File

@ -81,6 +81,22 @@ func (in *OIDCClient) DeepCopy() *OIDCClient {
return out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) { func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) {
*out = *in *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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) { func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) {
*out = *in *out = *in
if in.TLS != nil {
in, out := &in.TLS, &out.TLS
*out = new(TLSSpec)
**out = **in
}
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
out.Claims = in.Claims out.Claims = in.Claims
out.Client = in.Client out.Client = in.Client

View File

@ -98,6 +98,15 @@ spec:
minLength: 1 minLength: 1
pattern: ^https:// pattern: ^https://
type: string 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: required:
- client - client
- issuer - issuer

View File

@ -265,6 +265,11 @@ if ! tilt_mode; then
popd >/dev/null popd >/dev/null
fi fi
#
# Download the test CA bundle that was generated in the Dex pod.
#
test_ca_bundle_pem="$(kubectl exec -n dex deployment/dex -- cat /var/certs/ca.pem)"
# #
# Create the environment file # Create the environment file
# #
@ -287,7 +292,8 @@ export PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS='${supervisor_custom_labels}'
export PINNIPED_TEST_SUPERVISOR_HTTP_ADDRESS="127.0.0.1:12345" export PINNIPED_TEST_SUPERVISOR_HTTP_ADDRESS="127.0.0.1:12345"
export PINNIPED_TEST_SUPERVISOR_HTTPS_ADDRESS="localhost:12344" export PINNIPED_TEST_SUPERVISOR_HTTPS_ADDRESS="localhost:12344"
export PINNIPED_TEST_PROXY=http://127.0.0.1:12346 export PINNIPED_TEST_PROXY=http://127.0.0.1:12346
export PINNIPED_TEST_CLI_OIDC_ISSUER=http://dex.dex.svc.cluster.local/dex 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_CLIENT_ID=pinniped-cli
export PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT=48095 export PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT=48095
export PINNIPED_TEST_CLI_OIDC_USERNAME=pinny@example.com export PINNIPED_TEST_CLI_OIDC_USERNAME=pinny@example.com

View File

@ -6,7 +6,11 @@ package upstreamwatcher
import ( import (
"context" "context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt" "fmt"
"net/http"
"net/url" "net/url"
"sort" "sort"
"time" "time"
@ -48,10 +52,12 @@ const (
reasonMissingKeys = "SecretMissingKeys" reasonMissingKeys = "SecretMissingKeys"
reasonSuccess = "Success" reasonSuccess = "Success"
reasonUnreachable = "Unreachable" reasonUnreachable = "Unreachable"
reasonInvalidTLSConfig = "InvalidTLSConfig"
reasonInvalidResponse = "InvalidResponse" reasonInvalidResponse = "InvalidResponse"
// Errors that are generated by our reconcile process. // 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. // 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) 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 { type controller struct {
cache IDPCache cache IDPCache
log logr.Logger log logr.Logger
client pinnipedclientset.Interface client pinnipedclientset.Interface
providers idpinformers.UpstreamOIDCProviderInformer providers idpinformers.UpstreamOIDCProviderInformer
secrets corev1informers.SecretInformer 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. // New instantiates a new controllerlib.Controller which will populate the provided IDPCache.
@ -82,7 +114,7 @@ func New(
client: client, client: client,
providers: providers, providers: providers,
secrets: secrets, secrets: secrets,
validatorCache: cache.NewExpiring(), validatorCache: &lruValidatorCache{cache: cache.NewExpiring()},
} }
filter := pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()) filter := pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue())
return controllerlib.New( 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. // 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 { func (c *controller) validateIssuer(ctx context.Context, upstream *v1alpha1.UpstreamOIDCProvider, result *provider.UpstreamOIDCIdentityProvider) *v1alpha1.Condition {
// Get the provider (from cache if possible). // Get the provider (from cache if possible).
var discoveredProvider *oidc.Provider discoveredProvider := c.validatorCache.getProvider(&upstream.Spec)
if cached, ok := c.validatorCache.Get(upstream.Spec.Issuer); ok {
discoveredProvider = cached.(*oidc.Provider)
}
// If the provider does not exist in the cache, do a fresh discovery lookup and save to the cache. // If the provider does not exist in the cache, do a fresh discovery lookup and save to the cache.
if discoveredProvider == nil { if discoveredProvider == nil {
var err error tlsConfig, err := getTLSConfig(upstream)
discoveredProvider, err = oidc.NewProvider(ctx, upstream.Spec.Issuer) 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 { if err != nil {
return &v1alpha1.Condition{ return &v1alpha1.Condition{
Type: typeOIDCDiscoverySucceeded, Type: typeOIDCDiscoverySucceeded,
@ -216,7 +255,7 @@ func (c *controller) validateIssuer(ctx context.Context, upstream *v1alpha1.Upst
} }
// Update the cache with the newly discovered value. // 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. // 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) { func (c *controller) updateStatus(ctx context.Context, upstream *v1alpha1.UpstreamOIDCProvider, conditions []*v1alpha1.Condition) {
log := c.log.WithValues("namespace", upstream.Namespace, "name", upstream.Name) log := c.log.WithValues("namespace", upstream.Namespace, "name", upstream.Name)
updated := upstream.DeepCopy() updated := upstream.DeepCopy()

View File

@ -5,9 +5,9 @@ package upstreamwatcher
import ( import (
"context" "context"
"encoding/base64"
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/http/httptest"
"net/url" "net/url"
"strings" "strings"
"testing" "testing"
@ -25,6 +25,7 @@ import (
pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions" pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions"
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/testlogger" "go.pinniped.dev/internal/testutil/testlogger"
) )
@ -34,7 +35,8 @@ func TestController(t *testing.T) {
earlier := metav1.NewTime(now.Add(-1 * time.Hour).UTC()) earlier := metav1.NewTime(now.Add(-1 * time.Hour).UTC())
// Start another test server that answers discovery successfully. // 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") testIssuerAuthorizeURL, err := url.Parse("https://example.com/authorize")
require.NoError(t, err) require.NoError(t, err)
@ -65,7 +67,8 @@ func TestController(t *testing.T) {
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
Spec: v1alpha1.UpstreamOIDCProviderSpec{ Spec: v1alpha1.UpstreamOIDCProviderSpec{
Issuer: testIssuer.URL, Issuer: testIssuerURL,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
}, },
@ -106,7 +109,8 @@ func TestController(t *testing.T) {
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
Spec: v1alpha1.UpstreamOIDCProviderSpec{ Spec: v1alpha1.UpstreamOIDCProviderSpec{
Issuer: testIssuer.URL, Issuer: testIssuerURL,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
}, },
@ -151,7 +155,8 @@ func TestController(t *testing.T) {
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
Spec: v1alpha1.UpstreamOIDCProviderSpec{ Spec: v1alpha1.UpstreamOIDCProviderSpec{
Issuer: testIssuer.URL, Issuer: testIssuerURL,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, 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", name: "issuer is invalid URL",
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
@ -240,7 +341,8 @@ func TestController(t *testing.T) {
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
Spec: v1alpha1.UpstreamOIDCProviderSpec{ Spec: v1alpha1.UpstreamOIDCProviderSpec{
Issuer: testIssuer.URL + "/invalid", Issuer: testIssuerURL + "/invalid",
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
}, },
@ -285,7 +387,8 @@ func TestController(t *testing.T) {
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
Spec: v1alpha1.UpstreamOIDCProviderSpec{ Spec: v1alpha1.UpstreamOIDCProviderSpec{
Issuer: testIssuer.URL + "/insecure", Issuer: testIssuerURL + "/insecure",
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
}, },
@ -330,7 +433,8 @@ func TestController(t *testing.T) {
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name"}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name"},
Spec: v1alpha1.UpstreamOIDCProviderSpec{ Spec: v1alpha1.UpstreamOIDCProviderSpec{
Issuer: testIssuer.URL, Issuer: testIssuerURL,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")},
}, },
@ -373,7 +477,8 @@ func TestController(t *testing.T) {
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{ inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
Spec: v1alpha1.UpstreamOIDCProviderSpec{ Spec: v1alpha1.UpstreamOIDCProviderSpec{
Issuer: testIssuer.URL, Issuer: testIssuerURL,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
}, },
@ -486,10 +591,9 @@ func normalizeUpstreams(upstreams []v1alpha1.UpstreamOIDCProvider, now metav1.Ti
return result return result
} }
func newTestIssuer(t *testing.T) *httptest.Server { func newTestIssuer(t *testing.T) (string, string) {
mux := http.NewServeMux() mux := http.NewServeMux()
testServer := httptest.NewServer(mux) caBundlePEM, testURL := testutil.TLSTestServer(t, mux.ServeHTTP)
t.Cleanup(testServer.Close)
type providerJSON struct { type providerJSON struct {
Issuer string `json:"issuer"` 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) { mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json") w.Header().Set("content-type", "application/json")
_ = json.NewEncoder(w).Encode(&providerJSON{ _ = json.NewEncoder(w).Encode(&providerJSON{
Issuer: testServer.URL, Issuer: testURL,
AuthURL: "https://example.com/authorize", 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) { mux.HandleFunc("/invalid/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json") w.Header().Set("content-type", "application/json")
_ = json.NewEncoder(w).Encode(&providerJSON{ _ = json.NewEncoder(w).Encode(&providerJSON{
Issuer: testServer.URL + "/invalid", Issuer: testURL + "/invalid",
AuthURL: "%", 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) { mux.HandleFunc("/insecure/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json") w.Header().Set("content-type", "application/json")
_ = json.NewEncoder(w).Encode(&providerJSON{ _ = json.NewEncoder(w).Encode(&providerJSON{
Issuer: testServer.URL + "/insecure", Issuer: testURL + "/insecure",
AuthURL: "http://example.com/authorize", AuthURL: "http://example.com/authorize",
}) })
}) })
return testServer return caBundlePEM, testURL
} }

View File

@ -44,6 +44,8 @@ type handlerState struct {
scopes []string scopes []string
cache SessionCache cache SessionCache
httpClient *http.Client
// Parameters of the localhost listener. // Parameters of the localhost listener.
listenAddr string listenAddr string
callbackPath 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. // nopCache is a SessionCache that doesn't actually do anything.
type nopCache struct{} type nopCache struct{}
@ -144,6 +154,7 @@ func Login(issuer string, clientID string, opts ...Option) (*Token, error) {
callbackPath: "/callback", callbackPath: "/callback",
ctx: context.Background(), ctx: context.Background(),
callbacks: make(chan callbackResult), callbacks: make(chan callbackResult),
httpClient: http.DefaultClient,
// Default implementations of external dependencies (to be mocked in tests). // Default implementations of external dependencies (to be mocked in tests).
generateState: state.Generate, 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. // Always set a long, but non-infinite timeout for this operation.
ctx, cancel := context.WithTimeout(h.ctx, 10*time.Minute) ctx, cancel := context.WithTimeout(h.ctx, 10*time.Minute)
defer cancel() defer cancel()
ctx = oidc.ClientContext(ctx, h.httpClient)
h.ctx = ctx h.ctx = ctx
// Initialize login parameters. // Initialize login parameters.

View File

@ -416,6 +416,7 @@ func TestLogin(t *testing.T) {
require.Equal(t, []*Token{&testToken}, cache.sawPutTokens) require.Equal(t, []*Token{&testToken}, cache.sawPutTokens)
}) })
require.NoError(t, WithSessionCache(cache)(h)) require.NoError(t, WithSessionCache(cache)(h))
require.NoError(t, WithClient(&http.Client{Timeout: 10 * time.Second})(h))
h.openURL = func(actualURL string) error { h.openURL = func(actualURL string) error {
parsedActualURL, err := url.Parse(actualURL) parsedActualURL, err := url.Parse(actualURL)

View File

@ -6,13 +6,15 @@
#@ load("@ytt:yaml", "yaml") #@ load("@ytt:yaml", "yaml")
#@ def dexConfig(): #@ def dexConfig():
issuer: http://dex.dex.svc.cluster.local/dex issuer: https://dex.dex.svc.cluster.local/dex
storage: storage:
type: sqlite3 type: sqlite3
config: config:
file: ":memory:" file: ":memory:"
web: web:
http: 0.0.0.0:80 https: 0.0.0.0:443
tlsCert: /var/certs/dex.pem
tlsKey: /var/certs/dex-key.pem
oauth2: oauth2:
skipApprovalScreen: true skipApprovalScreen: true
staticClients: staticClients:
@ -67,24 +69,59 @@ spec:
annotations: annotations:
dexConfigHash: #@ sha256.sum(yaml.encode(dexConfig())) dexConfigHash: #@ sha256.sum(yaml.encode(dexConfig()))
spec: spec:
initContainers:
- name: generate-certs
image: cfssl/cfssl:1.5.0
imagePullPolicy: IfNotPresent
command: ["/bin/bash"]
args:
- -c
- |
cd /var/certs
cfssl print-defaults config > /tmp/cfssl-default.json
echo '{"CN": "Pinniped Test","hosts": [],"key": {"algo": "ecdsa","size": 256},"names": [{}]}' > csr.json
echo "generating CA key..."
cfssl genkey \
-config /tmp/cfssl-default.json \
-initca csr.json \
| cfssljson -bare ca
echo "generating Dex server certificate..."
cfssl gencert \
-ca ca.pem -ca-key ca-key.pem \
-config /tmp/cfssl-default.json \
-profile www \
-cn "dex.dex.svc.cluster.local" \
-hostname "dex.dex.svc.cluster.local" \
csr.json \
| cfssljson -bare dex
volumeMounts:
- name: certs
mountPath: /var/certs
containers: containers:
- name: dex - name: dex
image: quay.io/dexidp/dex:v2.10.0 image: quay.io/dexidp/dex:v2.10.0
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: command:
- /usr/local/bin/dex - /usr/local/bin/dex
- serve - serve
- /etc/dex/cfg/config.yaml - /etc/dex/cfg/config.yaml
ports: ports:
- name: http - name: https
containerPort: 80 containerPort: 443
volumeMounts: volumeMounts:
- name: config - name: dex-config
mountPath: /etc/dex/cfg mountPath: /etc/dex/cfg
- name: certs
mountPath: /var/certs
readOnly: true
volumes: volumes:
- name: config - name: dex-config
configMap: configMap:
name: dex-config name: dex-config
- name: certs
emptyDir: {}
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
@ -98,4 +135,5 @@ spec:
selector: selector:
app: dex app: dex
ports: ports:
- port: 80 - port: 443
name: https

View File

@ -130,8 +130,8 @@ func getLoginProvider(t *testing.T) *loginProviderPatterns {
}, },
{ {
Name: "Dex", Name: "Dex",
IssuerPattern: regexp.MustCompile(`\Ahttp://dex\.dex\.svc\.cluster\.local/dex.*\z`), IssuerPattern: regexp.MustCompile(`\Ahttps://dex\.dex\.svc\.cluster\.local/dex.*\z`),
LoginPagePattern: regexp.MustCompile(`\Ahttp://dex\.dex\.svc\.cluster\.local/dex/auth/local.+\z`), LoginPagePattern: regexp.MustCompile(`\Ahttps://dex\.dex\.svc\.cluster\.local/dex/auth/local.+\z`),
UsernameSelector: "input#login", UsernameSelector: "input#login",
PasswordSelector: "input#password", PasswordSelector: "input#password",
LoginButtonSelector: "button#submit-login", LoginButtonSelector: "button#submit-login",
@ -170,6 +170,7 @@ func TestCLILoginOIDC(t *testing.T) {
agouti.Desired(caps), agouti.Desired(caps),
agouti.ChromeOptions("args", []string{ agouti.ChromeOptions("args", []string{
"--no-sandbox", "--no-sandbox",
"--ignore-certificate-errors",
"--headless", // Comment out this line to see the tests happen in a visible browser window. "--headless", // Comment out this line to see the tests happen in a visible browser window.
}), }),
// Uncomment this to see stdout/stderr from chromedriver. // Uncomment this to see stdout/stderr from chromedriver.
@ -413,6 +414,15 @@ func oidcLoginCommand(ctx context.Context, t *testing.T, pinnipedExe string, ses
"--session-cache", sessionCachePath, "--session-cache", sessionCachePath,
"--skip-browser", "--skip-browser",
) )
// If there is a custom CA bundle, pass it via --ca-bundle and a temporary file.
if env.OIDCUpstream.CABundle != "" {
path := filepath.Join(t.TempDir(), "test-ca.pem")
require.NoError(t, ioutil.WriteFile(path, []byte(env.OIDCUpstream.CABundle), 0600))
cmd.Args = append(cmd.Args, "--ca-bundle", path)
}
// If there is a custom proxy, set it using standard environment variables.
if env.Proxy != "" { if env.Proxy != "" {
cmd.Env = append(os.Environ(), cmd.Env = append(os.Environ(),
"http_proxy="+env.Proxy, "http_proxy="+env.Proxy,

View File

@ -5,6 +5,7 @@ package integration
import ( import (
"context" "context"
"encoding/base64"
"testing" "testing"
"time" "time"
@ -17,7 +18,7 @@ import (
) )
func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) { func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) {
library.SkipUnlessIntegration(t) env := library.IntegrationEnv(t)
t.Run("invalid missing secret and bad issuer", func(t *testing.T) { t.Run("invalid missing secret and bad issuer", func(t *testing.T) {
t.Parallel() t.Parallel()
@ -50,7 +51,10 @@ func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) {
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
t.Parallel() t.Parallel()
spec := v1alpha1.UpstreamOIDCProviderSpec{ spec := v1alpha1.UpstreamOIDCProviderSpec{
Issuer: "https://accounts.google.com", // Use Google as an example of a valid OIDC issuer for now. Issuer: env.OIDCUpstream.Issuer,
TLS: &v1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.OIDCUpstream.CABundle)),
},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{ AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{
AdditionalScopes: []string{"email", "profile"}, AdditionalScopes: []string{"email", "profile"},
}, },

View File

@ -48,6 +48,7 @@ type TestEnv struct {
OIDCUpstream struct { OIDCUpstream struct {
Issuer string `json:"issuer"` Issuer string `json:"issuer"`
CABundle string `json:"caBundle" `
ClientID string `json:"clientID"` ClientID string `json:"clientID"`
LocalhostPort int `json:"localhostPort"` LocalhostPort int `json:"localhostPort"`
Username string `json:"username"` Username string `json:"username"`
@ -130,6 +131,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) {
result.Proxy = os.Getenv("PINNIPED_TEST_PROXY") result.Proxy = os.Getenv("PINNIPED_TEST_PROXY")
result.OIDCUpstream.Issuer = needEnv(t, "PINNIPED_TEST_CLI_OIDC_ISSUER") result.OIDCUpstream.Issuer = needEnv(t, "PINNIPED_TEST_CLI_OIDC_ISSUER")
result.OIDCUpstream.CABundle = os.Getenv("PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE")
result.OIDCUpstream.ClientID = needEnv(t, "PINNIPED_TEST_CLI_OIDC_CLIENT_ID") result.OIDCUpstream.ClientID = needEnv(t, "PINNIPED_TEST_CLI_OIDC_CLIENT_ID")
result.OIDCUpstream.LocalhostPort, _ = strconv.Atoi(needEnv(t, "PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT")) result.OIDCUpstream.LocalhostPort, _ = strconv.Atoi(needEnv(t, "PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT"))
result.OIDCUpstream.Username = needEnv(t, "PINNIPED_TEST_CLI_OIDC_USERNAME") result.OIDCUpstream.Username = needEnv(t, "PINNIPED_TEST_CLI_OIDC_USERNAME")