diff options
Diffstat (limited to 'main.go')
-rw-r--r-- | main.go | 118 |
1 files changed, 38 insertions, 80 deletions
@@ -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 { |