summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2017-08-31 04:28:14 +0000
committerMike Crute <mike@crute.us>2017-08-31 04:28:14 +0000
commit34d0f2d7e323acdc48cf91b0dc8514b6753de5d5 (patch)
tree10d41d475d60f8fe88dc4e43e2352362a36c1f86
parentb602fac5decc5daab15af6abf2dc6dfeb649c1d5 (diff)
downloadoidc_proxy-34d0f2d7e323acdc48cf91b0dc8514b6753de5d5.tar.bz2
oidc_proxy-34d0f2d7e323acdc48cf91b0dc8514b6753de5d5.tar.xz
oidc_proxy-34d0f2d7e323acdc48cf91b0dc8514b6753de5d5.zip
Implement JWKS fetching
-rw-r--r--cautious_http_client.go80
-rw-r--r--main.go119
2 files changed, 192 insertions, 7 deletions
diff --git a/cautious_http_client.go b/cautious_http_client.go
new file mode 100644
index 0000000..66179f2
--- /dev/null
+++ b/cautious_http_client.go
@@ -0,0 +1,80 @@
1package main
2
3import (
4 "encoding/json"
5 "net"
6 "net/http"
7 "net/url"
8 "time"
9)
10
11type CautiousHTTPClient interface {
12 Get(string) (*http.Response, error)
13 GetJSON(string, interface{}) error
14}
15
16type cautiousHttpClient struct {
17 client *http.Client
18}
19
20func NewCautiousHTTPClient() CautiousHTTPClient {
21 // May Need: TLSClientConfig *tls.Config
22 CautiousTransport := &http.Transport{
23 Proxy: http.ProxyFromEnvironment,
24 DialContext: (&net.Dialer{
25 Timeout: 1 * time.Second,
26 KeepAlive: 30 * time.Second,
27 DualStack: true,
28 }).DialContext,
29 MaxIdleConns: 100,
30 IdleConnTimeout: 90 * time.Second,
31 TLSHandshakeTimeout: 3 * time.Second,
32 ExpectContinueTimeout: 1 * time.Second,
33 ResponseHeaderTimeout: 5 * time.Second,
34 MaxResponseHeaderBytes: 500000, // .5 MB
35 }
36
37 return &cautiousHttpClient{
38 client: &http.Client{
39 Transport: CautiousTransport,
40 Timeout: 30 * time.Second,
41 },
42 }
43}
44
45func (c *cautiousHttpClient) Get(gurl string) (*http.Response, error) {
46 u, err := url.Parse(gurl)
47 if err != nil {
48 return nil, err
49 }
50
51 // TODO
52 /*
53 if u.Scheme != "https" {
54 return nil, fmt.Errorf("URL for GET must be secure")
55 }
56 */
57
58 r, err := c.client.Get(u.String())
59 if err != nil {
60 return nil, err
61 }
62 r.Body = http.MaxBytesReader(nil, r.Body, 1000000)
63 return r, err
64}
65
66func (c *cautiousHttpClient) GetJSON(url string, rv interface{}) error {
67 r, err := c.Get(url)
68 if err != nil {
69 return err
70 }
71 defer r.Body.Close()
72
73 d := json.NewDecoder(r.Body)
74 err = d.Decode(rv)
75 if err != nil {
76 return err
77 }
78
79 return nil
80}
diff --git a/main.go b/main.go
index a96f0eb..44501c0 100644
--- a/main.go
+++ b/main.go
@@ -4,7 +4,8 @@ import (
4 "context" 4 "context"
5 "crypto/rand" 5 "crypto/rand"
6 "encoding/hex" 6 "encoding/hex"
7 _ "github.com/dgrijalva/jwt-go" 7 "fmt"
8 "gopkg.in/square/go-jose.v2"
8 "log" 9 "log"
9 "net/http" 10 "net/http"
10 "net/http/httputil" 11 "net/http/httputil"
@@ -33,12 +34,58 @@ type ProxyConfig struct {
33 UpstreamURL string 34 UpstreamURL string
34 ListenOn string 35 ListenOn string
35 TrustedCACert string 36 TrustedCACert string
37 IsOptional bool
36 RequestMFA bool 38 RequestMFA bool
37 AllowedMFAMethods []string // An OR set 39 AllowedMFAMethods []string // An OR set
38 RequiredMFAMethods []string // An AND set 40 RequiredMFAMethods []string // An AND set
39 reverseProxy *httputil.ReverseProxy 41 reverseProxy *httputil.ReverseProxy
40} 42}
41 43
44type IdPConfig struct {
45 AuthorizationEndpoint string `json:"authorization_endpoint"`
46 Issuer string `json:"issuer"`
47 JwksUri string `json:"jwks_uri"`
48 SupportedGrantTypes []string `json:"grant_types_supported"`
49 IdTokenSigningAlgs []string `json:"id_token_signing_alg_values_supported"`
50 ResponseModes []string `json:"response_modes_supported"`
51 ResponseTypes []string `json:"response_types_supported"`
52 Scopes []string `json:"scopes_supported"`
53 SubjectTypes []string `json:"subject_types_supported"`
54}
55
56func FetchIdPConfig(h CautiousHTTPClient, idp_url string) (*IdPConfig, error) {
57 u, err := url.Parse(idp_url)
58 if err != nil {
59 return nil, err
60 }
61 u.Path = "/.well-known/openid-configuration"
62
63 var idpc IdPConfig
64 err = h.GetJSON(u.String(), &idpc)
65 if err != nil {
66 return nil, err
67 }
68
69 return &idpc, nil
70}
71
72// TODO: Optimization to fetch only if expired (per http headers)
73func FetchJWKS(h CautiousHTTPClient, jwks_url string) (map[string]jose.JSONWebKey, error) {
74 var jwks jose.JSONWebKeySet
75 err := h.GetJSON(jwks_url, &jwks)
76 if err != nil {
77 return nil, err
78 }
79
80 keys := make(map[string]jose.JSONWebKey, len(jwks.Keys))
81
82 for _, k := range jwks.Keys {
83 keys[k.KeyID] = k
84 }
85
86 return keys, nil
87}
88
42func URLMustParse(u string) *url.URL { 89func URLMustParse(u string) *url.URL {
43 o, err := url.Parse(u) 90 o, err := url.Parse(u)
44 if err != nil { 91 if err != nil {
@@ -91,15 +138,18 @@ func DownloadCRL() {
91func ValidateCertificate() { 138func ValidateCertificate() {
92} 139}
93 140
141// TODO
94func MakeClientID(r *http.Request) string { 142func MakeClientID(r *http.Request) string {
95 if strings.Contains(r.Host, ":") { 143 if strings.Contains(r.Host, ":") {
96 return r.Host 144 return r.Host
97 } 145 }
146 return ""
98} 147}
99 148
100// TODO 149// TODO
101func RedirectToIDP(w http.ResponseWriter, r *http.Request) { 150func RedirectToIDP(w http.ResponseWriter, r *http.Request) {
102 nonce := GenerateNonce() 151 nonce, _ := GenerateNonce()
152 _ = nonce
103 nonceh := "" // SHA256 nonce 153 nonceh := "" // SHA256 nonce
104 154
105 // Set nonce cookie 155 // Set nonce cookie
@@ -117,13 +167,34 @@ func SetTokenCookieAndRedirect(w http.ResponseWriter, r *http.Request, token str
117} 167}
118 168
119// TODO 169// 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
186func RefreshIDPConfig() {
187}
188
189// TODO
190// If x5u exists in header
120// Fetch cert from x5u URL 191// Fetch cert from x5u URL
121// Get CRL from cert, fetch (connect timeout 1s, read timeout 30s, read size 1M) 192// Get CRL from cert, fetch (connect timeout 1s, read timeout 30s, read size 1M)
122// 193//
123// exp claim has passed +- 5 minutes 194// exp claim has passed +- 5 minutes
124// iat claim is greater than 24 hours +- 5 minutes 195// iat claim is greater than 24 hours +- 5 minutes
125// aud claim is exact match for client_id 196// aud claim is exact match for client_id
126// iss claim is exact match for idp (ex: foo.example.com 197// iss claim is exact match for idp (ex: foo.example.com)
127// if other aud claims validate that they are known 198// if other aud claims validate that they are known
128// nonce in JWT must be SHA256 of rfp cookie value 199// nonce in JWT must be SHA256 of rfp cookie value
129// Validate cert 200// Validate cert
@@ -151,8 +222,9 @@ func RequestHasForwardedUser(w http.ResponseWriter, r *http.Request) bool {
151 } 222 }
152} 223}
153 224
154func RequestIsOverSecureChannel(w http.ResponseWriter, r *http.Request) { 225func RequestIsOverSecureChannel(w http.ResponseWriter, r *http.Request) bool {
155 if https, ok := r.Header["X-Forwarded-Proto"]; !ok || len(https) != 1 { 226 https, ok := r.Header["X-Forwarded-Proto"]
227 if !ok || len(https) != 1 {
156 log.Printf("ERROR: Request does not contain X-Forwarded-Proto header") 228 log.Printf("ERROR: Request does not contain X-Forwarded-Proto header")
157 http.Error(w, "Bad Request", http.StatusBadRequest) 229 http.Error(w, "Bad Request", http.StatusBadRequest)
158 return false 230 return false
@@ -202,13 +274,13 @@ func AuthProxyController(w http.ResponseWriter, r *http.Request) {
202 tokenc, err := r.Cookie(TOKEN_COOKIE_NAME) 274 tokenc, err := r.Cookie(TOKEN_COOKIE_NAME)
203 if err != nil { 275 if err != nil {
204 log.Printf("ERROR: No token cookie") 276 log.Printf("ERROR: No token cookie")
205 RedirectToIDP() 277 RedirectToIDP(w, r)
206 return 278 return
207 } 279 }
208 280
209 if !ValidateJWT(tokenc.Value, rfpc.Value) { 281 if !ValidateJWT(tokenc.Value, rfpc.Value) {
210 log.Printf("ERROR: Token is invalid") 282 log.Printf("ERROR: Token is invalid")
211 RedirectToIDP() 283 RedirectToIDP(w, r)
212 return 284 return
213 } 285 }
214 286
@@ -235,6 +307,15 @@ func LogoutController(w http.ResponseWriter, r *http.Request) {
235} 307}
236 308
237// TODO 309// TODO
310func LoginController(w http.ResponseWriter, r *http.Request) {
311}
312
313// TODO
314// Optional login allows for applications that can operate in anonymous mode or
315// authenticated mode. When in anonmyous mode the request is proxied through
316// without an X-Forwarded-User header. Upstream servers should either expose or
317// map a URL for /.oidc/login to allow users to login. On successful login the
318// user will be redirected back to the main page for the site (/)
238func parseConfig() *ProxyConfig { 319func parseConfig() *ProxyConfig {
239 return &ProxyConfig{ 320 return &ProxyConfig{
240 IDProviderURL: "", 321 IDProviderURL: "",
@@ -242,13 +323,37 @@ func parseConfig() *ProxyConfig {
242 UpstreamURL: "http://localhost:9991/", 323 UpstreamURL: "http://localhost:9991/",
243 ListenOn: ":9992", 324 ListenOn: ":9992",
244 TrustedCACert: "", 325 TrustedCACert: "",
326 IsOptional: false,
245 } 327 }
246} 328}
247 329
248func main() { 330func main() {
331 h := NewCautiousHTTPClient()
332
333 idpc, err := FetchIdPConfig(h, "http://mcrute-virt:9993")
334 if err != nil {
335 fmt.Printf("%s\n", err)
336 return
337 }
338
339 jwks, err := FetchJWKS(h, idpc.JwksUri)
340 if err != nil {
341 fmt.Printf("%s\n", err)
342 return
343 }
344 fmt.Printf("%+v\n", jwks)
345 return
346
249 cfg := parseConfig() 347 cfg := parseConfig()
250 cfg.reverseProxy = httputil.NewSingleHostReverseProxy(URLMustParse(cfg.UpstreamURL)) 348 cfg.reverseProxy = httputil.NewSingleHostReverseProxy(URLMustParse(cfg.UpstreamURL))
251 349
350 if cfg.IsOptional {
351 http.HandleFunc("/.oidc/login", func(w http.ResponseWriter, r *http.Request) {
352 LoginController(w,
353 r.WithContext(context.WithValue(r.Context(), "ProxyConfig", cfg)))
354 })
355 }
356
252 http.HandleFunc("/.oidc/logout", func(w http.ResponseWriter, r *http.Request) { 357 http.HandleFunc("/.oidc/logout", func(w http.ResponseWriter, r *http.Request) {
253 LogoutController(w, 358 LogoutController(w,
254 r.WithContext(context.WithValue(r.Context(), "ProxyConfig", cfg))) 359 r.WithContext(context.WithValue(r.Context(), "ProxyConfig", cfg)))