Skip to content

Commit cf0dac2

Browse files
authored
Merge pull request #37 from agilezebra/36-add-root-cas
add rootCAs option
2 parents ae38e7b + 6eca763 commit cf0dac2

File tree

3 files changed

+116
-3
lines changed

3 files changed

+116
-3
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ experimental:
1919
plugins:
2020
jwt:
2121
moduleName: github.com/agilezebra/jwt-middleware
22-
version: v1.2.7
22+
version: v1.2.8
2323
```
2424
1b. or with command-line options:
2525
2626
```yaml
2727
command:
2828
...
2929
- "--experimental.plugins.jwt.modulename=github.com/agilezebra/jwt-middleware"
30-
- "--experimental.plugins.jwt.version=v1.2.7"
30+
- "--experimental.plugins.jwt.version=v1.2.8"
3131
```
3232
3333
2) Configure and activate the plugin as a middleware in your dynamic traefik config:
@@ -70,6 +70,7 @@ Name | Description
7070
`forwardToken` | Boolean indicating whether the token should be forwarded to the backend. Default true. If multiple tokens are present in different locations (e.g. cookie and header) and forwarding is false, only the token used will be removed.
7171
`optional` | Validate tokens according to the normal rules but don't require that a token be present. If specific claim requirements are specified in `require` but with `optional` set to `true` and a token is not present, access will be permitted even though the requirements are obviously not met, which may not be what you want or expect. In this case, no headers will be set from claims (as there aren't any). This is quite a niche case but is intended for use on endpoints that support both authorized and anonymous access and you want JWTs verified if present.
7272
`insecureSkipVerify` | A list of issuers' domains for which TLS certificates should not be verified (i.e. use `InsecureSkipVerify: true`). Only the hostname/domain should be specified (i.e. no scheme or trailing slash). Applies to both the openid-configuration and jwks calls.
73+
`rootCAs` | One or more additional root certificate authorities, in PEM format, to be combined with the system cert pool when verifying server certificates.
7374
`infoToStdout` | traefik does not yet have support for plugins to use the logger so, by default, all messages are logged using `log.Printf`, which will send messages from the plugin out as if they were logged at `ERROR` level. This may be irritating for those that don't like to see non-error messages show up as if they are errors. There is a workaround available in that the plugin can send messages to STDOUT and traefik will log these as if they were logged at `DEBUG` level. Setting `infoToStdout` to `true` will send all non-error info messages to STDOUT and these will appear in logs at `DEBUG` level. These will obviously only appear if you set your traefik log level to `DEBUG` (which may actually be more irritating if you don't want the spew that this creates, so this option is not enabled by default). Note also that this workaround does not appear to be working correctly in traefik v2 and in this case you may not see info messages at all if you enable this.
7475

7576
### Template Interpolation

jwt.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"crypto/tls"
7+
"crypto/x509"
78
"fmt"
89
"html"
910
"html/template"
@@ -26,6 +27,7 @@ type Config struct {
2627
Issuers []string `json:"issuers,omitempty"`
2728
SkipPrefetch bool `json:"skipPrefetch,omitempty"`
2829
InsecureSkipVerify []string `json:"insecureSkipVerify,omitempty"`
30+
RootCAs []string `json:"rootCAs,omitempty"`
2931
Secret string `json:"secret,omitempty"`
3032
Secrets map[string]string `json:"secrets,omitempty"`
3133
Require map[string]interface{} `json:"require,omitempty"`
@@ -155,7 +157,7 @@ func New(_ context.Context, next http.Handler, config *Config, name string) (htt
155157
secret: secret,
156158
issuers: canonicalizeDomains(config.Issuers),
157159
clients: createClients(config.InsecureSkipVerify),
158-
defaultClient: &http.Client{},
160+
defaultClient: createDefaultClient(config.RootCAs, true),
159161
require: convertRequire(config.Require),
160162
keys: make(map[string]interface{}),
161163
issuerKeys: make(map[string]map[string]interface{}),
@@ -569,6 +571,29 @@ func canonicalizeDomains(domains []string) []string {
569571
return domains
570572
}
571573

574+
// createDefaultClient returns an http.Client with the given root CAs, or a default client if no root CAs are provided.
575+
func createDefaultClient(pems []string, useSystemCertPool bool) *http.Client {
576+
if pems == nil {
577+
return &http.Client{}
578+
}
579+
certs, _ := x509.SystemCertPool()
580+
if certs == nil || !useSystemCertPool {
581+
// We don't plan an option to set useSystemCertPool=false but it helps with test coverage
582+
certs = x509.NewCertPool()
583+
}
584+
for _, pem := range pems {
585+
if !certs.AppendCertsFromPEM([]byte(pem)) {
586+
log.Printf("failed to add root CA:\n%s", pem)
587+
}
588+
}
589+
transport := &http.Transport{
590+
TLSClientConfig: &tls.Config{
591+
RootCAs: certs,
592+
},
593+
}
594+
return &http.Client{Transport: transport}
595+
}
596+
572597
// createClients reads a list of domains in the InsecureSkipVerify configuration and creates a map of domains to http.Client with InsecureSkipVerify set.
573598
func createClients(insecureSkipVerify []string) map[string]*http.Client {
574599
// Create a single client with InsecureSkipVerify set

jwt_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,6 +1015,54 @@ func TestServeHTTP(tester *testing.T) {
10151015
Method: jwt.SigningMethodES256,
10161016
HeaderName: "Authorization",
10171017
},
1018+
{
1019+
Name: "RootCAs",
1020+
Expect: http.StatusOK,
1021+
Config: `
1022+
issuers:
1023+
- "https://127.0.0.1/"
1024+
rootCAs: |
1025+
-----BEGIN CERTIFICATE-----
1026+
MIIDJzCCAg+gAwIBAgIUDDYN8pGCpUC6tsqDW4meIXsmN04wDQYJKoZIhvcNAQEL
1027+
BQAwIzELMAkGA1UEBhMCVUsxFDASBgNVBAoMC0FnaWxlIFplYnJhMB4XDTI1MDMx
1028+
MTE0MTU1MloXDTM1MDMwOTE0MTU1MlowIzELMAkGA1UEBhMCVUsxFDASBgNVBAoM
1029+
C0FnaWxlIFplYnJhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA70Gs
1030+
A3QEKB94Eqyt+V07qDNtykhlyOLSiGIRk1/Slr5B1mTY8Mt88gg8MFldyVukjze+
1031+
/5GT/lZ3plMMiA7wnpJ683iWqMVOzQTtYlgcMknnrRJhHuDIGmcdakudXl484emE
1032+
9iz+cWgl2cw1rb0rtNC1koQ90MohcTqW+5By0TUaulf80ZcJbGFG8LTqVKVJatET
1033+
QedgrYR3tIR6VRtj7pnFZ1w9gZhpPL26mrMg3Wk3GHf/j48jebHVYbeuuSoBXJX8
1034+
rGmfCtwzMWqyZvMU9MRP6KpPu20UIOuzau6JyD22RhlLSrX/1eI9Et0IMqEF/iM/
1035+
EGpTGDJTeX3bJavzAQIDAQABo1MwUTAdBgNVHQ4EFgQUwR3igK8QvKXQ3JuGlYUc
1036+
1jHwBqUwHwYDVR0jBBgwFoAUwR3igK8QvKXQ3JuGlYUc1jHwBqUwDwYDVR0TAQH/
1037+
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAoEgu6gQTf8Br0Id7Jp6Oht6XSG0o
1038+
RtYJ4SwWD0U1acJpWKgtTkBA9cfGMYngFzUe9Xmxt1iBSCJtbQ/SQj5x0vcXsoR0
1039+
zWBnihf3XERnJOyLWR7cUCfVYEu0xFCNrc1m5Wzj4IG2NJBTtiIiAdnTbEcBd7hk
1040+
f7Vy+al187qn3HQcwdRfMatjFrrM92tHvd79VJsZcgj8Yl3QcgZFIQ2O+PtrXxLR
1041+
2auMwVTxdRe0QUT6zvtZGf1niNH5s8DBVeDWqBArlC7M/HuLj6QOIMDEI2aC3yS1
1042+
LT12fZ0MWBjfGc90EEJ9z4/CRUWMdtlOaLnXinyrvOH+SSTJD8xfwKqH6g==
1043+
-----END CERTIFICATE-----
1044+
require:
1045+
aud: test`,
1046+
Claims: `{"aud": "test"}`,
1047+
Method: jwt.SigningMethodES256,
1048+
HeaderName: "Authorization",
1049+
},
1050+
{
1051+
Name: "Bad RootCAs",
1052+
Expect: http.StatusOK,
1053+
Config: `
1054+
issuers:
1055+
- "https://127.0.0.1/"
1056+
rootCAs: |
1057+
-----BEGIN CERTIFICATE-----
1058+
bad
1059+
-----END CERTIFICATE-----
1060+
require:
1061+
aud: test`,
1062+
Claims: `{"aud": "test"}`,
1063+
Method: jwt.SigningMethodES256,
1064+
HeaderName: "Authorization",
1065+
},
10181066
{
10191067
Name: "infoToStdout",
10201068
Expect: http.StatusOK,
@@ -1517,6 +1565,45 @@ func TestHostname(tester *testing.T) {
15171565
}
15181566
}
15191567

1568+
func TestCreateDefaultClient(tester *testing.T) {
1569+
pems := []string{
1570+
`\
1571+
-----BEGIN CERTIFICATE-----
1572+
MIIDJzCCAg+gAwIBAgIUDDYN8pGCpUC6tsqDW4meIXsmN04wDQYJKoZIhvcNAQEL
1573+
BQAwIzELMAkGA1UEBhMCVUsxFDASBgNVBAoMC0FnaWxlIFplYnJhMB4XDTI1MDMx
1574+
MTE0MTU1MloXDTM1MDMwOTE0MTU1MlowIzELMAkGA1UEBhMCVUsxFDASBgNVBAoM
1575+
C0FnaWxlIFplYnJhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA70Gs
1576+
A3QEKB94Eqyt+V07qDNtykhlyOLSiGIRk1/Slr5B1mTY8Mt88gg8MFldyVukjze+
1577+
/5GT/lZ3plMMiA7wnpJ683iWqMVOzQTtYlgcMknnrRJhHuDIGmcdakudXl484emE
1578+
9iz+cWgl2cw1rb0rtNC1koQ90MohcTqW+5By0TUaulf80ZcJbGFG8LTqVKVJatET
1579+
QedgrYR3tIR6VRtj7pnFZ1w9gZhpPL26mrMg3Wk3GHf/j48jebHVYbeuuSoBXJX8
1580+
rGmfCtwzMWqyZvMU9MRP6KpPu20UIOuzau6JyD22RhlLSrX/1eI9Et0IMqEF/iM/
1581+
EGpTGDJTeX3bJavzAQIDAQABo1MwUTAdBgNVHQ4EFgQUwR3igK8QvKXQ3JuGlYUc
1582+
1jHwBqUwHwYDVR0jBBgwFoAUwR3igK8QvKXQ3JuGlYUc1jHwBqUwDwYDVR0TAQH/
1583+
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAoEgu6gQTf8Br0Id7Jp6Oht6XSG0o
1584+
RtYJ4SwWD0U1acJpWKgtTkBA9cfGMYngFzUe9Xmxt1iBSCJtbQ/SQj5x0vcXsoR0
1585+
zWBnihf3XERnJOyLWR7cUCfVYEu0xFCNrc1m5Wzj4IG2NJBTtiIiAdnTbEcBd7hk
1586+
f7Vy+al187qn3HQcwdRfMatjFrrM92tHvd79VJsZcgj8Yl3QcgZFIQ2O+PtrXxLR
1587+
2auMwVTxdRe0QUT6zvtZGf1niNH5s8DBVeDWqBArlC7M/HuLj6QOIMDEI2aC3yS1
1588+
LT12fZ0MWBjfGc90EEJ9z4/CRUWMdtlOaLnXinyrvOH+SSTJD8xfwKqH6g==
1589+
-----END CERTIFICATE-----`,
1590+
}
1591+
tester.Run("Default", func(tester *testing.T) {
1592+
client := createDefaultClient(nil, true)
1593+
if client == nil {
1594+
tester.Error("client is nil")
1595+
}
1596+
client = createDefaultClient(pems, true)
1597+
if client == nil {
1598+
tester.Error("client is nil")
1599+
}
1600+
client = createDefaultClient(pems, false)
1601+
if client == nil {
1602+
tester.Error("client is nil")
1603+
}
1604+
})
1605+
}
1606+
15201607
func BenchmarkServeHTTP(benchmark *testing.B) {
15211608
test := Test{
15221609
Name: "SigningMethodRS256 passes",

0 commit comments

Comments
 (0)