Skip to content

Commit c43b5ca

Browse files
committed
Add support for CA certificate for auth provider (#2957)
When using an SSO provider with a certificate signed by our own internal CA, the ui server is currently unable to verify the certificate. This change adds support for providing a CA certificate to enable verification of the used certificate.
1 parent e828b14 commit c43b5ca

7 files changed

Lines changed: 115 additions & 7 deletions

File tree

server/config/development.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ auth:
3131
issuerUrl: "" # needed if the Issuer Url and the Provider Url are different
3232
clientId: xxxxxxxxxxxxxxxxxxxx
3333
clientSecret: xxxxxxxxxxxxxxxxxxxx
34+
caFile:
35+
caData:
3436
scopes:
3537
- openid
3638
- profile

server/config/docker.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ auth:
5858
clientSecret: {{ env "TEMPORAL_AUTH_CLIENT_SECRET" }}
5959
callbackUrl: {{ env "TEMPORAL_AUTH_CALLBACK_URL" }}
6060
useIdTokenAsBearer: {{ env "TEMPORAL_AUTH_USE_ID_TOKEN_AS_BEARER" | default "false" }}
61+
caFile: {{ env "TEMPORAL_AUTH_CA" | default "" }}
62+
caData: {{ env "TEMPORAL_AUTH_CA_DATA" | default "" }}
6163
scopes:
6264
{{- if env "TEMPORAL_AUTH_SCOPES" }}
6365
{{- range env "TEMPORAL_AUTH_SCOPES" | split "," }}

server/server/config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ type (
129129
Options map[string]interface{} `yaml:"options"`
130130
// UseIDTokenAsBearer - Use ID token instead of access token as Bearer in Authorization header
131131
UseIDTokenAsBearer bool `yaml:"useIdTokenAsBearer"`
132+
// CaFile - optional custom CA bundle for contacting the auth provider
133+
CaFile string `yaml:"caFile"`
134+
// CaData - optional base64-encoded CA bundle for contacting the auth provider
135+
CaData string `yaml:"caData"`
132136
}
133137

134138
Codec struct {

server/server/route/auth.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
package route
2424

2525
import (
26+
"crypto/tls"
2627
"encoding/base64"
2728
"encoding/json"
2829
"errors"
@@ -38,6 +39,7 @@ import (
3839
"github.com/labstack/echo/v4"
3940
"github.com/temporalio/ui-server/v2/server/auth"
4041
"github.com/temporalio/ui-server/v2/server/config"
42+
"github.com/temporalio/ui-server/v2/server/rpc"
4143
"golang.org/x/net/context"
4244
"golang.org/x/oauth2"
4345
)
@@ -75,22 +77,40 @@ func SetAuthRoutes(e *echo.Echo, cfgProvider *config.ConfigProviderWithRefresh)
7577
ctx := context.Background()
7678
serverCfg, err := cfgProvider.GetConfig()
7779
if err != nil {
78-
fmt.Printf("unable to get auth config: %s\n", err)
80+
log.Printf("unable to get auth config: %s\n", err)
81+
return
7982
}
8083

8184
if !serverCfg.Auth.Enabled {
8285
return
8386
}
8487

85-
if len(serverCfg.Auth.Providers) == 0 {
86-
log.Fatal(`auth providers configuration is empty. Configure an auth provider or disable auth`)
88+
err = validateAuthConfig(&serverCfg.Auth)
89+
if err != nil {
90+
log.Fatalf("invalid auth config: %s\n", err)
8791
}
8892

8993
providerCfg := serverCfg.Auth.Providers[0] // only single provider is currently supported
9094

9195
if len(providerCfg.IssuerUrl) > 0 {
9296
ctx = oidc.InsecureIssuerURLContext(ctx, providerCfg.IssuerUrl)
9397
}
98+
99+
// Configure HTTP client (with timeout) and optional custom CA if provided via caFile or caData
100+
httpClient := &http.Client{
101+
Timeout: 30 * time.Second,
102+
}
103+
if providerCfg.CaFile != "" || providerCfg.CaData != "" {
104+
caCertPool, err := rpc.LoadCACert(providerCfg.CaFile, providerCfg.CaData)
105+
if err != nil {
106+
log.Fatalf("Unable to load auth CA certificate: %s\n", err)
107+
}
108+
transport := http.DefaultTransport.(*http.Transport).Clone()
109+
transport.TLSClientConfig = &tls.Config{RootCAs: caCertPool}
110+
httpClient.Transport = transport
111+
}
112+
ctx = oidc.ClientContext(ctx, httpClient)
113+
94114
provider, err := oidc.NewProvider(ctx, providerCfg.ProviderURL)
95115
if err != nil {
96116
log.Fatal(err)
@@ -368,3 +388,16 @@ type Nonce struct {
368388
Nonce string `json:"nonce"`
369389
ReturnURL string `json:"return_url"`
370390
}
391+
392+
func validateAuthConfig(cfg *config.Auth) error {
393+
if len(cfg.Providers) == 0 {
394+
return fmt.Errorf(`auth providers configuration is empty. Configure an auth provider or disable auth`)
395+
}
396+
for _, providerCfg := range cfg.Providers {
397+
if providerCfg.CaFile != "" && providerCfg.CaData != "" {
398+
return fmt.Errorf("cannot specify Auth CA file and CA data at the same time")
399+
}
400+
}
401+
402+
return nil
403+
}

server/server/route/auth_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package route
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/temporalio/ui-server/v2/server/config"
8+
)
9+
10+
func TestValidateAuthConfig_NoProviders(t *testing.T) {
11+
err := validateAuthConfig(&config.Auth{
12+
Enabled: true,
13+
Providers: []config.AuthProvider{},
14+
})
15+
16+
assert.Error(t, err)
17+
}
18+
19+
func TestValidateAuthConfig_CaFileAndCaDataMutuallyExclusive(t *testing.T) {
20+
err := validateAuthConfig(&config.Auth{
21+
Enabled: true,
22+
Providers: []config.AuthProvider{
23+
{
24+
CaFile: "file",
25+
CaData: "data",
26+
},
27+
},
28+
})
29+
30+
assert.Error(t, err)
31+
}
32+
33+
func TestValidateAuthConfig_ValidConfig(t *testing.T) {
34+
err := validateAuthConfig(&config.Auth{
35+
Enabled: true,
36+
Providers: []config.AuthProvider{
37+
{
38+
ProviderURL: "https://example.com",
39+
ClientID: "id",
40+
CallbackURL: "https://example.com/callback",
41+
CaFile: "file",
42+
},
43+
},
44+
})
45+
46+
assert.NoError(t, err)
47+
}

server/server/rpc/tls.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ func CreateTLSConfig(address string, cfg *config.TLS) (*tls.Config, error) {
139139
}
140140

141141
if configureCertPool {
142-
caCertPool, err := loadCACert(cfg)
142+
caCertPool, err := LoadCACert(cfg.CaFile, cfg.CaData)
143143
if err != nil {
144144
log.Fatalf("Unable to load server CA certificate")
145145
return nil, err
@@ -171,9 +171,12 @@ func CreateTLSConfig(address string, cfg *config.TLS) (*tls.Config, error) {
171171
return tlsConfig, nil
172172
}
173173

174-
func loadCACert(cfg *config.TLS) (caPool *x509.CertPool, err error) {
175-
pathOrUrl := cfg.CaFile
176-
caData := cfg.CaData
174+
// LoadCACert loads a CA certificate bundle from a file path or HTTPS URL, or from base64-encoded data,
175+
// and returns a certificate pool containing the parsed certificates.
176+
func LoadCACert(pathOrUrl string, caData string) (caPool *x509.CertPool, err error) {
177+
if pathOrUrl == "" && caData == "" {
178+
return nil, errors.New("no CA certificate source provided")
179+
}
177180

178181
caPool = x509.NewCertPool()
179182
var caBytes []byte

server/server/rpc/tls_cert_loader_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"crypto/tls"
77
"crypto/x509"
88
"crypto/x509/pkix"
9+
"encoding/base64"
910
"encoding/pem"
1011
"math/big"
1112
"os"
@@ -81,6 +82,22 @@ func TestCertLoader_ReloadsNewKeyPair(t *testing.T) {
8182
}
8283
}
8384

85+
func TestLoadCACert_FromBase64Data(t *testing.T) {
86+
certPEM, _ := generateCertKeyPair(t, "ca")
87+
88+
caData := base64.StdEncoding.EncodeToString(certPEM)
89+
90+
pool, err := LoadCACert("", caData)
91+
assert.NoError(t, err)
92+
assert.NotNil(t, pool)
93+
}
94+
95+
func TestLoadCACert_EmptyInputsError(t *testing.T) {
96+
pool, err := LoadCACert("", "")
97+
assert.EqualError(t, err, "no CA certificate source provided")
98+
assert.Nil(t, pool)
99+
}
100+
84101
func generateCertKeyPair(t *testing.T, commonName string) (certPEM, keyPEM []byte) {
85102
key, err := rsa.GenerateKey(rand.Reader, 2048)
86103
assert.NoError(t, err)

0 commit comments

Comments
 (0)