Skip to content

Commit 79a9167

Browse files
committed
Add support for CA certificate for auth provider
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 34fdd3a commit 79a9167

7 files changed

Lines changed: 112 additions & 7 deletions

File tree

server/config/development.yaml

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

server/config/docker.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ auth:
5959
clientSecret: {{ env "TEMPORAL_AUTH_CLIENT_SECRET" }}
6060
callbackUrl: {{ env "TEMPORAL_AUTH_CALLBACK_URL" }}
6161
useIdTokenAsBearer: {{ env "TEMPORAL_AUTH_USE_ID_TOKEN_AS_BEARER" | default "false" }}
62+
caFile: {{ env "TEMPORAL_AUTH_CA" | default "" }}
63+
caData: {{ env "TEMPORAL_AUTH_CA_DATA" | default "" }}
6264
scopes:
6365
{{- if env "TEMPORAL_AUTH_SCOPES" }}
6466
{{- 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
@@ -126,6 +126,10 @@ type (
126126
Options map[string]interface{} `yaml:"options"`
127127
// UseIDTokenAsBearer - Use ID token instead of access token as Bearer in Authorization header
128128
UseIDTokenAsBearer bool `yaml:"useIdTokenAsBearer"`
129+
// CaFile - optional custom CA bundle for contacting the auth provider
130+
CaFile string `yaml:"caFile"`
131+
// CaData - optional base64-encoded CA bundle for contacting the auth provider
132+
CaData string `yaml:"caData"`
129133
}
130134

131135
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"
@@ -37,6 +38,7 @@ import (
3738
"github.com/labstack/echo/v4"
3839
"github.com/temporalio/ui-server/v2/server/auth"
3940
"github.com/temporalio/ui-server/v2/server/config"
41+
"github.com/temporalio/ui-server/v2/server/rpc"
4042
"golang.org/x/net/context"
4143
"golang.org/x/oauth2"
4244
)
@@ -46,22 +48,40 @@ func SetAuthRoutes(e *echo.Echo, cfgProvider *config.ConfigProviderWithRefresh)
4648
ctx := context.Background()
4749
serverCfg, err := cfgProvider.GetConfig()
4850
if err != nil {
49-
fmt.Printf("unable to get auth config: %s\n", err)
51+
log.Printf("unable to get auth config: %s\n", err)
52+
return
5053
}
5154

5255
if !serverCfg.Auth.Enabled {
5356
return
5457
}
5558

56-
if len(serverCfg.Auth.Providers) == 0 {
57-
log.Fatal(`auth providers configuration is empty. Configure an auth provider or disable auth`)
59+
err = validateAuthConfig(&serverCfg.Auth)
60+
if err != nil {
61+
log.Fatalf("invalid auth config: %s\n", err)
5862
}
5963

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

6266
if len(providerCfg.IssuerUrl) > 0 {
6367
ctx = oidc.InsecureIssuerURLContext(ctx, providerCfg.IssuerUrl)
6468
}
69+
70+
// Configure HTTP client (with timeout) and optional custom CA if provided via caFile or caData
71+
httpClient := &http.Client{
72+
Timeout: 30 * time.Second,
73+
}
74+
if providerCfg.CaFile != "" || providerCfg.CaData != "" {
75+
caCertPool, err := rpc.LoadCACert(providerCfg.CaFile, providerCfg.CaData)
76+
if err != nil {
77+
log.Fatalf("Unable to load auth CA certificate: %s\n", err)
78+
}
79+
httpClient.Transport = &http.Transport{
80+
TLSClientConfig: &tls.Config{RootCAs: caCertPool},
81+
}
82+
}
83+
ctx = oidc.ClientContext(ctx, httpClient)
84+
6585
provider, err := oidc.NewProvider(ctx, providerCfg.ProviderURL)
6686
if err != nil {
6787
log.Fatal(err)
@@ -231,3 +251,16 @@ type Nonce struct {
231251
Nonce string `json:"nonce"`
232252
ReturnURL string `json:"return_url"`
233253
}
254+
255+
func validateAuthConfig(cfg *config.Auth) error {
256+
if len(cfg.Providers) == 0 {
257+
return fmt.Errorf(`auth providers configuration is empty. Configure an auth provider or disable auth`)
258+
}
259+
for _, providerCfg := range cfg.Providers {
260+
if providerCfg.CaFile != "" && providerCfg.CaData != "" {
261+
return fmt.Errorf("cannot specify Auth CA file and CA data at the same time")
262+
}
263+
}
264+
265+
return nil
266+
}

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: 4 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,9 @@ 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) {
177177

178178
caPool = x509.NewCertPool()
179179
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"
@@ -63,6 +64,22 @@ func TestCertLoader_ReloadsNewKeyPair(t *testing.T) {
6364
assert.NotEqual(t, expect1.Certificate, loaded2.Certificate)
6465
}
6566

67+
func TestLoadCACert_FromBase64Data(t *testing.T) {
68+
certPEM, _ := generateCertKeyPair(t, "ca")
69+
70+
caData := base64.StdEncoding.EncodeToString(certPEM)
71+
72+
pool, err := LoadCACert("", caData)
73+
assert.NoError(t, err)
74+
assert.NotNil(t, pool)
75+
}
76+
77+
func TestLoadCACert_EmptyInputsError(t *testing.T) {
78+
pool, err := LoadCACert("", "")
79+
assert.Error(t, err)
80+
assert.Nil(t, pool)
81+
}
82+
6683
// generateCertKeyPair creates a self-signed certificate and private key for testing.
6784
func generateCertKeyPair(t *testing.T, commonName string) (certPEM, keyPEM []byte) {
6885
// Generate a private key

0 commit comments

Comments
 (0)