diff options
author | Mike Crute <mike@crute.us> | 2017-09-05 03:52:50 +0000 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2017-09-05 03:52:50 +0000 |
commit | b7867d9cf5b0dd175b8167a552b830ebfe47d0ed (patch) | |
tree | 4b52c7461c7d9c48d68bec78cac6d06ae5940d28 | |
parent | 34d0f2d7e323acdc48cf91b0dc8514b6753de5d5 (diff) | |
download | oidc_proxy-b7867d9cf5b0dd175b8167a552b830ebfe47d0ed.tar.bz2 oidc_proxy-b7867d9cf5b0dd175b8167a552b830ebfe47d0ed.tar.xz oidc_proxy-b7867d9cf5b0dd175b8167a552b830ebfe47d0ed.zip |
Finish JWS and Cert validation
-rw-r--r-- | cautious_http_client.go | 13 | ||||
-rw-r--r-- | jws_validator.go | 104 | ||||
-rw-r--r-- | key_validator.go | 144 | ||||
-rw-r--r-- | main.go | 118 | ||||
-rwxr-xr-x | oidc_proxy | bin | 0 -> 6495319 bytes | |||
-rw-r--r-- | util.go | 43 |
6 files changed, 335 insertions, 87 deletions
diff --git a/cautious_http_client.go b/cautious_http_client.go index 66179f2..2f33ae0 100644 --- a/cautious_http_client.go +++ b/cautious_http_client.go | |||
@@ -2,6 +2,7 @@ package main | |||
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "encoding/json" | 4 | "encoding/json" |
5 | "fmt" | ||
5 | "net" | 6 | "net" |
6 | "net/http" | 7 | "net/http" |
7 | "net/url" | 8 | "net/url" |
@@ -28,9 +29,9 @@ func NewCautiousHTTPClient() CautiousHTTPClient { | |||
28 | }).DialContext, | 29 | }).DialContext, |
29 | MaxIdleConns: 100, | 30 | MaxIdleConns: 100, |
30 | IdleConnTimeout: 90 * time.Second, | 31 | IdleConnTimeout: 90 * time.Second, |
31 | TLSHandshakeTimeout: 3 * time.Second, | 32 | TLSHandshakeTimeout: 1 * time.Second, |
32 | ExpectContinueTimeout: 1 * time.Second, | 33 | ExpectContinueTimeout: 1 * time.Second, |
33 | ResponseHeaderTimeout: 5 * time.Second, | 34 | ResponseHeaderTimeout: 10 * time.Second, |
34 | MaxResponseHeaderBytes: 500000, // .5 MB | 35 | MaxResponseHeaderBytes: 500000, // .5 MB |
35 | } | 36 | } |
36 | 37 | ||
@@ -49,11 +50,9 @@ func (c *cautiousHttpClient) Get(gurl string) (*http.Response, error) { | |||
49 | } | 50 | } |
50 | 51 | ||
51 | // TODO | 52 | // TODO |
52 | /* | 53 | if u.Scheme != "https" && false { |
53 | if u.Scheme != "https" { | 54 | return nil, fmt.Errorf("URL for GET must be secure") |
54 | return nil, fmt.Errorf("URL for GET must be secure") | 55 | } |
55 | } | ||
56 | */ | ||
57 | 56 | ||
58 | r, err := c.client.Get(u.String()) | 57 | r, err := c.client.Get(u.String()) |
59 | if err != nil { | 58 | if err != nil { |
diff --git a/jws_validator.go b/jws_validator.go new file mode 100644 index 0000000..e77c026 --- /dev/null +++ b/jws_validator.go | |||
@@ -0,0 +1,104 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "crypto/sha256" | ||
5 | "encoding/hex" | ||
6 | "fmt" | ||
7 | "gopkg.in/square/go-jose.v2" | ||
8 | "gopkg.in/square/go-jose.v2/jwt" | ||
9 | "time" | ||
10 | ) | ||
11 | |||
12 | type Claims struct { | ||
13 | Nonce string `json:"nonce,omitempty"` | ||
14 | jwt.Claims | ||
15 | } | ||
16 | |||
17 | type JWSValidator interface { | ||
18 | Validate(string, string) (*Claims, error) | ||
19 | } | ||
20 | |||
21 | type jwsValidator struct { | ||
22 | algorithms *stringSet | ||
23 | jwks map[string]jose.JSONWebKey | ||
24 | issuer string | ||
25 | clientID string | ||
26 | clockSkew time.Duration | ||
27 | maxLifetime time.Duration | ||
28 | } | ||
29 | |||
30 | // TODO | ||
31 | // validate amr claim contains requested acr values (selective_mfa will be just mfa) | ||
32 | // validate acr claim is the same as requested acr_values | ||
33 | func NewJWSValidator(jwks map[string]jose.JSONWebKey, issuer string, client_id string, skew time.Duration, max_life time.Duration) JWSValidator { | ||
34 | return &jwsValidator{ | ||
35 | algorithms: NewStringSet("PS256", "PS385", "PS512"), | ||
36 | jwks: jwks, | ||
37 | issuer: issuer, | ||
38 | clientID: client_id, | ||
39 | clockSkew: skew, | ||
40 | maxLifetime: max_life, | ||
41 | } | ||
42 | } | ||
43 | |||
44 | func (v *jwsValidator) Validate(j string, nonce string) (*Claims, error) { | ||
45 | parsed_jwt, err := jwt.ParseSigned(j) | ||
46 | if err != nil { | ||
47 | return nil, err | ||
48 | } | ||
49 | |||
50 | if len(parsed_jwt.Headers) != 1 { | ||
51 | return nil, fmt.Errorf("Invalid signature count") | ||
52 | } | ||
53 | |||
54 | head := parsed_jwt.Headers[0] | ||
55 | |||
56 | if !v.algorithms.Contains(head.Algorithm) { | ||
57 | return nil, fmt.Errorf("Invalid signature algorithm") | ||
58 | } | ||
59 | |||
60 | if typ, ok := head.ExtraHeaders[jose.HeaderType]; !ok || typ != "JWS" { | ||
61 | return nil, fmt.Errorf("Invalid token type") | ||
62 | } | ||
63 | |||
64 | key, ok := v.jwks[head.KeyID] | ||
65 | if !ok { | ||
66 | return nil, fmt.Errorf("No key found for key id") | ||
67 | } | ||
68 | |||
69 | claims := &Claims{} | ||
70 | if err = parsed_jwt.Claims(key, claims); err != nil { | ||
71 | return nil, err | ||
72 | } | ||
73 | |||
74 | exp := jwt.Expected{ | ||
75 | Issuer: v.issuer, | ||
76 | Audience: jwt.Audience{v.clientID}, | ||
77 | Time: time.Now(), | ||
78 | } | ||
79 | |||
80 | if err := claims.ValidateWithLeeway(exp, v.clockSkew); err != nil { | ||
81 | return nil, err | ||
82 | } | ||
83 | |||
84 | if claims.IssuedAt.Time().Add(v.maxLifetime).Before(time.Now()) { | ||
85 | return nil, fmt.Errorf("Token exceeded max lifetime") | ||
86 | } | ||
87 | |||
88 | if err = v.validateNonce(nonce, claims.Nonce); err != nil { | ||
89 | return nil, err | ||
90 | } | ||
91 | |||
92 | return claims, nil | ||
93 | } | ||
94 | |||
95 | func (v *jwsValidator) validateNonce(nonce string, token_nonce string) error { | ||
96 | s256 := sha256.New() | ||
97 | s256.Write([]byte(nonce)) | ||
98 | hashed_nonce := hex.EncodeToString(s256.Sum(nil)) | ||
99 | if token_nonce != hashed_nonce { | ||
100 | return fmt.Errorf("Invalid nonce") | ||
101 | } | ||
102 | |||
103 | return nil | ||
104 | } | ||
diff --git a/key_validator.go b/key_validator.go new file mode 100644 index 0000000..fe6eb7b --- /dev/null +++ b/key_validator.go | |||
@@ -0,0 +1,144 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "crypto/rsa" | ||
5 | "crypto/x509" | ||
6 | "encoding/pem" | ||
7 | "fmt" | ||
8 | "gopkg.in/square/go-jose.v2" | ||
9 | "io/ioutil" | ||
10 | ) | ||
11 | |||
12 | type KeyValidator interface { | ||
13 | Validate(jose.JSONWebKey) error | ||
14 | LoadRootPEM(string) error | ||
15 | } | ||
16 | |||
17 | type keyValidator struct { | ||
18 | pkiSubject string | ||
19 | algorithms *stringSet | ||
20 | roots *x509.CertPool | ||
21 | } | ||
22 | |||
23 | func NewKeyValidator(subject string) KeyValidator { | ||
24 | return &keyValidator{ | ||
25 | pkiSubject: subject, | ||
26 | algorithms: NewStringSet("PS256", "PS385", "PS512"), | ||
27 | roots: x509.NewCertPool(), | ||
28 | } | ||
29 | } | ||
30 | |||
31 | func (v *keyValidator) LoadRootPEM(filename string) error { | ||
32 | pem_data, err := ioutil.ReadFile(filename) | ||
33 | if err != nil { | ||
34 | return err | ||
35 | } | ||
36 | |||
37 | pem_block, _ := pem.Decode(pem_data) | ||
38 | if pem_block == nil { | ||
39 | return fmt.Errorf("PEM decode failed") | ||
40 | } | ||
41 | |||
42 | cert, err := x509.ParseCertificate(pem_block.Bytes) | ||
43 | if err != nil { | ||
44 | return err | ||
45 | } | ||
46 | |||
47 | v.roots.AddCert(cert) | ||
48 | |||
49 | return nil | ||
50 | } | ||
51 | |||
52 | func (v *keyValidator) Validate(key jose.JSONWebKey) error { | ||
53 | pk, ok := key.Key.(*rsa.PublicKey) | ||
54 | if !ok { | ||
55 | return fmt.Errorf("Key type is not RSA") | ||
56 | } | ||
57 | |||
58 | if !v.algorithms.Contains(key.Algorithm) { | ||
59 | return fmt.Errorf("Key algorithm is not supported") | ||
60 | } | ||
61 | |||
62 | cert := key.Certificates[0] | ||
63 | cpk, ok := cert.PublicKey.(*rsa.PublicKey) | ||
64 | if !ok { | ||
65 | return fmt.Errorf("Public key is not RSA") | ||
66 | } | ||
67 | |||
68 | if cpk.N.BitLen() < 2048 { | ||
69 | return fmt.Errorf("Key length less than 2048 bits") | ||
70 | } | ||
71 | |||
72 | if cert.KeyUsage&x509.KeyUsageDigitalSignature != 1 { | ||
73 | return fmt.Errorf("Certificate not valid for digital signatures") | ||
74 | } | ||
75 | |||
76 | err := v.validateCertificateChain(key.Certificates) | ||
77 | if err != nil { | ||
78 | return err | ||
79 | } | ||
80 | |||
81 | err = v.validateCertificateCRL(cert) | ||
82 | if err != nil { | ||
83 | return err | ||
84 | } | ||
85 | |||
86 | err = v.validatePublicKeyInCertificate(pk, cpk) | ||
87 | if err != nil { | ||
88 | return err | ||
89 | } | ||
90 | |||
91 | return nil | ||
92 | } | ||
93 | |||
94 | // TODO | ||
95 | // Fetch CRL from distrubtion point in cert | ||
96 | // Validate CRL signed by trusted CA | ||
97 | // Validate cert not in CRL | ||
98 | func (v *keyValidator) validateCertificateCRL(cert *x509.Certificate) error { | ||
99 | return nil | ||
100 | } | ||
101 | |||
102 | func (v *keyValidator) validateCertificateChain(chain []*x509.Certificate) error { | ||
103 | vo := x509.VerifyOptions{ | ||
104 | Roots: v.roots, | ||
105 | KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, | ||
106 | } | ||
107 | |||
108 | if len(chain) > 1 { | ||
109 | ip := x509.NewCertPool() | ||
110 | for _, i := range chain[1:] { | ||
111 | ip.AddCert(i) | ||
112 | } | ||
113 | |||
114 | vo.Intermediates = ip | ||
115 | } | ||
116 | |||
117 | chains, err := chain[0].Verify(vo) | ||
118 | if err != nil { | ||
119 | return err | ||
120 | } | ||
121 | |||
122 | if len(chains) <= 0 { | ||
123 | return fmt.Errorf("No valid certificate chains found") | ||
124 | } | ||
125 | |||
126 | if chain[0].Subject.CommonName != v.pkiSubject { | ||
127 | return fmt.Errorf("Invalid certificate subject name") | ||
128 | } | ||
129 | |||
130 | return nil | ||
131 | } | ||
132 | |||
133 | // validate first item of x5c matches n and e | ||
134 | func (v *keyValidator) validatePublicKeyInCertificate(pk *rsa.PublicKey, cpk *rsa.PublicKey) error { | ||
135 | if cpk.E != pk.E { | ||
136 | return fmt.Errorf("E in key and E in cert do not match") | ||
137 | } | ||
138 | |||
139 | if pk.N.Cmp(cpk.N) != 0 { | ||
140 | return fmt.Errorf("N in key and N in cert do not match") | ||
141 | } | ||
142 | |||
143 | return nil | ||
144 | } | ||
@@ -11,6 +11,7 @@ import ( | |||
11 | "net/http/httputil" | 11 | "net/http/httputil" |
12 | "net/url" | 12 | "net/url" |
13 | "strings" | 13 | "strings" |
14 | "time" | ||
14 | ) | 15 | ) |
15 | 16 | ||
16 | const ( | 17 | const ( |
@@ -19,6 +20,8 @@ const ( | |||
19 | RFP_COOKIE_NAME string = "sso_rfp" | 20 | RFP_COOKIE_NAME string = "sso_rfp" |
20 | ) | 21 | ) |
21 | 22 | ||
23 | // TODO: Enable https checks in HTTP client | ||
24 | |||
22 | // acr_values can be mfa or selective_mfa (mfa only for external users) | 25 | // acr_values can be mfa or selective_mfa (mfa only for external users) |
23 | // mfa amr values: | 26 | // mfa amr values: |
24 | // pas - password | 27 | // pas - password |
@@ -34,6 +37,9 @@ type ProxyConfig struct { | |||
34 | UpstreamURL string | 37 | UpstreamURL string |
35 | ListenOn string | 38 | ListenOn string |
36 | TrustedCACert string | 39 | TrustedCACert string |
40 | PKISubject string // TODO: Should be same as IDP w/out scheme and port | ||
41 | ClockSkew time.Duration | ||
42 | MaxLiftetime time.Duration | ||
37 | IsOptional bool | 43 | IsOptional bool |
38 | RequestMFA bool | 44 | RequestMFA bool |
39 | AllowedMFAMethods []string // An OR set | 45 | AllowedMFAMethods []string // An OR set |
@@ -45,7 +51,7 @@ type IdPConfig struct { | |||
45 | AuthorizationEndpoint string `json:"authorization_endpoint"` | 51 | AuthorizationEndpoint string `json:"authorization_endpoint"` |
46 | Issuer string `json:"issuer"` | 52 | Issuer string `json:"issuer"` |
47 | JwksUri string `json:"jwks_uri"` | 53 | JwksUri string `json:"jwks_uri"` |
48 | SupportedGrantTypes []string `json:"grant_types_supported"` | 54 | GrantTypes []string `json:"grant_types_supported"` |
49 | IdTokenSigningAlgs []string `json:"id_token_signing_alg_values_supported"` | 55 | IdTokenSigningAlgs []string `json:"id_token_signing_alg_values_supported"` |
50 | ResponseModes []string `json:"response_modes_supported"` | 56 | ResponseModes []string `json:"response_modes_supported"` |
51 | ResponseTypes []string `json:"response_types_supported"` | 57 | ResponseTypes []string `json:"response_types_supported"` |
@@ -53,6 +59,7 @@ type IdPConfig struct { | |||
53 | SubjectTypes []string `json:"subject_types_supported"` | 59 | SubjectTypes []string `json:"subject_types_supported"` |
54 | } | 60 | } |
55 | 61 | ||
62 | // TODO: Optimization to fetch only if expired (per http headers) | ||
56 | func FetchIdPConfig(h CautiousHTTPClient, idp_url string) (*IdPConfig, error) { | 63 | func FetchIdPConfig(h CautiousHTTPClient, idp_url string) (*IdPConfig, error) { |
57 | u, err := url.Parse(idp_url) | 64 | u, err := url.Parse(idp_url) |
58 | if err != nil { | 65 | if err != nil { |
@@ -70,7 +77,7 @@ func FetchIdPConfig(h CautiousHTTPClient, idp_url string) (*IdPConfig, error) { | |||
70 | } | 77 | } |
71 | 78 | ||
72 | // TODO: Optimization to fetch only if expired (per http headers) | 79 | // TODO: Optimization to fetch only if expired (per http headers) |
73 | func FetchJWKS(h CautiousHTTPClient, jwks_url string) (map[string]jose.JSONWebKey, error) { | 80 | func FetchJWKS(h CautiousHTTPClient, jwks_url string, val KeyValidator) (map[string]jose.JSONWebKey, error) { |
74 | var jwks jose.JSONWebKeySet | 81 | var jwks jose.JSONWebKeySet |
75 | err := h.GetJSON(jwks_url, &jwks) | 82 | err := h.GetJSON(jwks_url, &jwks) |
76 | if err != nil { | 83 | if err != nil { |
@@ -80,20 +87,15 @@ func FetchJWKS(h CautiousHTTPClient, jwks_url string) (map[string]jose.JSONWebKe | |||
80 | keys := make(map[string]jose.JSONWebKey, len(jwks.Keys)) | 87 | keys := make(map[string]jose.JSONWebKey, len(jwks.Keys)) |
81 | 88 | ||
82 | for _, k := range jwks.Keys { | 89 | for _, k := range jwks.Keys { |
83 | keys[k.KeyID] = k | 90 | err = val.Validate(k) |
91 | if err == nil { | ||
92 | keys[k.KeyID] = k | ||
93 | } | ||
84 | } | 94 | } |
85 | 95 | ||
86 | return keys, nil | 96 | return keys, nil |
87 | } | 97 | } |
88 | 98 | ||
89 | func URLMustParse(u string) *url.URL { | ||
90 | o, err := url.Parse(u) | ||
91 | if err != nil { | ||
92 | panic(err) | ||
93 | } | ||
94 | return o | ||
95 | } | ||
96 | |||
97 | func GenerateNonce() (string, error) { | 99 | func GenerateNonce() (string, error) { |
98 | nonce := make([]byte, NONCE_SIZE) | 100 | nonce := make([]byte, NONCE_SIZE) |
99 | n, err := rand.Read(nonce) | 101 | n, err := rand.Read(nonce) |
@@ -103,10 +105,6 @@ func GenerateNonce() (string, error) { | |||
103 | return hex.EncodeToString(nonce), nil | 105 | return hex.EncodeToString(nonce), nil |
104 | } | 106 | } |
105 | 107 | ||
106 | func CompareUpper(lhs, rhs string) bool { | ||
107 | return strings.ToUpper(lhs) == strings.ToUpper(rhs) | ||
108 | } | ||
109 | |||
110 | // TODO | 108 | // TODO |
111 | // Cookie rules | 109 | // Cookie rules |
112 | // Secure | 110 | // Secure |
@@ -117,28 +115,6 @@ func SetCookie() { | |||
117 | } | 115 | } |
118 | 116 | ||
119 | // TODO | 117 | // TODO |
120 | // Fetch (connect timeout 1s, read timeout 30s, read size 1M) | ||
121 | func DownloadCertificate() { | ||
122 | } | ||
123 | |||
124 | // TODO | ||
125 | // Fetch (connect timeout 1s, read timeout 30s, read size 1M) | ||
126 | func DownloadCRL() { | ||
127 | } | ||
128 | |||
129 | // TODO | ||
130 | // Cert validation | ||
131 | // Validate cert not in CRL | ||
132 | // Validate cert chains to trusted CA cert (ship with proxy) | ||
133 | // Validate CRL signed by trusted CA | ||
134 | // Cert "Subject CN " must match exactly PKI setting (ex: foo-pki.foo.com) | ||
135 | // Current time bust be within "Validity Not Before" and "Validity Not After" in cert +- 5 minutes | ||
136 | // Cert Key length >= 2048 | ||
137 | // Certificate usage must include "digitalSignature" | ||
138 | func ValidateCertificate() { | ||
139 | } | ||
140 | |||
141 | // TODO | ||
142 | func MakeClientID(r *http.Request) string { | 118 | func MakeClientID(r *http.Request) string { |
143 | if strings.Contains(r.Host, ":") { | 119 | if strings.Contains(r.Host, ":") { |
144 | return r.Host | 120 | return r.Host |
@@ -167,42 +143,6 @@ func SetTokenCookieAndRedirect(w http.ResponseWriter, r *http.Request, token str | |||
167 | } | 143 | } |
168 | 144 | ||
169 | // TODO | 145 | // TODO |
170 | // Occasionally refresh IDP config (per HTTP caching headers) | ||
171 | // | ||
172 | // Fetch ${IDP_HOST}/.well-known/openid-configuration | ||
173 | // - validate certificate chains to a trusted root | ||
174 | // - validate scopes_supported contains "openid" | ||
175 | // - validate response_types_supported contains "id_token" | ||
176 | // - validate grant_types_supported contains "implicit" | ||
177 | // - validate id_token_signing_alg_values_supported contains a supported signing type (see below) | ||
178 | // - Cache authorization_endpoint for redirecting users | ||
179 | // | ||
180 | // Fetch jwks_uri endpoint | ||
181 | // - Build key map indexed by kid for all keys that are suppored by our rules | ||
182 | // - kty == RSA | ||
183 | // - alg header must be one of [PS256, PS385, PS512] | ||
184 | // - pem decode x5c and validate the certificate chain as below | ||
185 | // - validate first item of x5c matches n and e | ||
186 | func RefreshIDPConfig() { | ||
187 | } | ||
188 | |||
189 | // TODO | ||
190 | // If x5u exists in header | ||
191 | // Fetch cert from x5u URL | ||
192 | // Get CRL from cert, fetch (connect timeout 1s, read timeout 30s, read size 1M) | ||
193 | // | ||
194 | // exp claim has passed +- 5 minutes | ||
195 | // iat claim is greater than 24 hours +- 5 minutes | ||
196 | // aud claim is exact match for client_id | ||
197 | // iss claim is exact match for idp (ex: foo.example.com) | ||
198 | // if other aud claims validate that they are known | ||
199 | // nonce in JWT must be SHA256 of rfp cookie value | ||
200 | // Validate cert | ||
201 | // alg jwt header must be one of [PS256, PS385, PS512] | ||
202 | // typ jwt header must be JWS | ||
203 | // validate jwt signature | ||
204 | // validate amr claim contains requested acr values (selective_mfa will be just mfa) | ||
205 | // validate acr claim is the same as requested acr_values | ||
206 | func ValidateJWT(jwt, rfp string) bool { | 146 | func ValidateJWT(jwt, rfp string) bool { |
207 | return true | 147 | return true |
208 | } | 148 | } |
@@ -318,33 +258,51 @@ func LoginController(w http.ResponseWriter, r *http.Request) { | |||
318 | // user will be redirected back to the main page for the site (/) | 258 | // user will be redirected back to the main page for the site (/) |
319 | func parseConfig() *ProxyConfig { | 259 | func parseConfig() *ProxyConfig { |
320 | return &ProxyConfig{ | 260 | return &ProxyConfig{ |
321 | IDProviderURL: "", | 261 | IDProviderURL: "http://mcrute-virt:9993", |
322 | ClientID: "", | 262 | ClientID: "test.crute.me:443", |
323 | UpstreamURL: "http://localhost:9991/", | 263 | UpstreamURL: "http://localhost:9991/", |
324 | ListenOn: ":9992", | 264 | ListenOn: ":9992", |
325 | TrustedCACert: "", | 265 | TrustedCACert: "/home/mcrute/oidc_project/test_ca/ca_cert.pem", |
326 | IsOptional: false, | 266 | IsOptional: false, |
267 | PKISubject: "Crute OpenID Signing 1", | ||
268 | MaxLiftetime: 24 * time.Hour, | ||
269 | ClockSkew: 5 * time.Minute, | ||
327 | } | 270 | } |
328 | } | 271 | } |
329 | 272 | ||
330 | func main() { | 273 | func main() { |
274 | cfg := parseConfig() | ||
331 | h := NewCautiousHTTPClient() | 275 | h := NewCautiousHTTPClient() |
332 | 276 | ||
333 | idpc, err := FetchIdPConfig(h, "http://mcrute-virt:9993") | 277 | v := NewKeyValidator(cfg.PKISubject) |
278 | v.LoadRootPEM(cfg.TrustedCACert) | ||
279 | |||
280 | idpc, err := FetchIdPConfig(h, cfg.IDProviderURL) | ||
334 | if err != nil { | 281 | if err != nil { |
335 | fmt.Printf("%s\n", err) | 282 | fmt.Printf("%s\n", err) |
336 | return | 283 | return |
337 | } | 284 | } |
338 | 285 | ||
339 | jwks, err := FetchJWKS(h, idpc.JwksUri) | 286 | jwks, err := FetchJWKS(h, idpc.JwksUri, v) |
340 | if err != nil { | 287 | if err != nil { |
341 | fmt.Printf("%s\n", err) | 288 | fmt.Printf("%s\n", err) |
342 | return | 289 | return |
343 | } | 290 | } |
344 | fmt.Printf("%+v\n", jwks) | 291 | |
292 | jv := NewJWSValidator(jwks, idpc.Issuer, cfg.ClientID, cfg.ClockSkew, cfg.MaxLiftetime) | ||
293 | |||
294 | nonce := "ofspmfjuvoswhhde" | ||
295 | raw_jwt := "eyJ0eXAiOiJKV1MiLCJhbGciOiJQUzI1NiIsImtpZCI6IjEifQ.eyJub25jZSI6IjM0MjlhMjAyYzU4ZDkyYjQwNjNjOWM4MWM2MjQyNGRlNzBkMmIzZDQ4MmVlNDFhOTdjYmNhZjEwZDk5MWFiOTMiLCJpc3MiOiJpZHAuY3J1dGUubWU6NDQzIiwiaWF0IjoxNTA0NTc2Mzc0LCJuYmYiOjE1MDQ1NzYzNzQsImV4cCI6MTUwNDY2Mjc3NCwic3ViIjoibWNydXRlIiwiYXVkIjoidGVzdC5jcnV0ZS5tZTo0NDMifQ.iizlNfY1Vg7d-XRmgyYuhpNkNrOGaT9OOgO0HdjBozOWMvKzBTtATbIfoWOrNH6DiFY1as8uy3I1Pxnkrb8Ti8_cLDQeLxOv9klAbnebeuPI_wtZ0iwSUnSWaYzN6I6sqcEjHX3fibFvAQhO5dNDzSwONjw4AvcdpZKh579FO1sAvIw-1DmMyPSUun7rbC0Kf1Jtdlr3q7tOp3wdI_erkstxCNPwyuv7X1J7uetsu0BeJS25C2DxeB03BPEIUoo_C1xvcqikfSLLpoFcyToYiS-R9o-WpRjGid_yug65J5ALn2aM3vhe9rRbydKVm_omGL8-Etj06zbqM0Y6OrJUgA" | ||
296 | claims, err := jv.Validate(raw_jwt, nonce) | ||
297 | if err != nil { | ||
298 | fmt.Printf("Error validating: %s\n", err) | ||
299 | return | ||
300 | } | ||
301 | |||
302 | fmt.Printf("Valid JWT for: %+v\n", claims.Subject) | ||
303 | |||
345 | return | 304 | return |
346 | 305 | ||
347 | cfg := parseConfig() | ||
348 | cfg.reverseProxy = httputil.NewSingleHostReverseProxy(URLMustParse(cfg.UpstreamURL)) | 306 | cfg.reverseProxy = httputil.NewSingleHostReverseProxy(URLMustParse(cfg.UpstreamURL)) |
349 | 307 | ||
350 | if cfg.IsOptional { | 308 | if cfg.IsOptional { |
diff --git a/oidc_proxy b/oidc_proxy new file mode 100755 index 0000000..e5df267 --- /dev/null +++ b/oidc_proxy | |||
Binary files differ | |||
@@ -0,0 +1,43 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "net/url" | ||
5 | "strings" | ||
6 | ) | ||
7 | |||
8 | type stringSet struct { | ||
9 | values map[string]bool | ||
10 | } | ||
11 | |||
12 | func NewStringSet(values ...string) *stringSet { | ||
13 | s := &stringSet{ | ||
14 | values: make(map[string]bool, len(values)), | ||
15 | } | ||
16 | |||
17 | for _, v := range values { | ||
18 | s.Add(v) | ||
19 | } | ||
20 | |||
21 | return s | ||
22 | } | ||
23 | |||
24 | func (s *stringSet) Add(v string) { | ||
25 | s.values[v] = true | ||
26 | } | ||
27 | |||
28 | func (s *stringSet) Contains(k string) bool { | ||
29 | _, ok := s.values[k] | ||
30 | return ok | ||
31 | } | ||
32 | |||
33 | func URLMustParse(u string) *url.URL { | ||
34 | o, err := url.Parse(u) | ||
35 | if err != nil { | ||
36 | panic(err) | ||
37 | } | ||
38 | return o | ||
39 | } | ||
40 | |||
41 | func CompareUpper(lhs, rhs string) bool { | ||
42 | return strings.ToUpper(lhs) == strings.ToUpper(rhs) | ||
43 | } | ||