summaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'main.go')
-rw-r--r--main.go409
1 files changed, 235 insertions, 174 deletions
diff --git a/main.go b/main.go
index 965e72c..805c40d 100644
--- a/main.go
+++ b/main.go
@@ -4,53 +4,52 @@ import (
4 "context" 4 "context"
5 "crypto/rand" 5 "crypto/rand"
6 "encoding/hex" 6 "encoding/hex"
7 "fmt" 7 "flag"
8 "gopkg.in/square/go-jose.v2" 8 "github.com/golang/glog"
9 "log" 9 "github.com/gorilla/handlers"
10 "github.com/pkg/errors"
10 "net/http" 11 "net/http"
11 "net/http/httputil" 12 "net/http/httputil"
12 "net/url" 13 "net/url"
14 "os"
15 "strconv"
13 "strings" 16 "strings"
14 "time" 17 "time"
15) 18)
16 19
17const ( 20const (
18 NONCE_SIZE int = 16 21 NONCE_SIZE = 16
19 TOKEN_COOKIE_NAME string = "sso_token" 22 TOKEN_COOKIE_NAME = "sso_token"
20 RFP_COOKIE_NAME string = "sso_rfp" 23 RFP_COOKIE_NAME = "sso_rfp"
24 DEFAULT_CLOCK_SKEW = 5 * time.Minute
25 DEFAULT_MAX_LIFETIME = 24 * time.Hour
26 DEFAULT_COOKIE_EXP = 48 * time.Hour
21) 27)
22 28
23// TODO: Enable https checks in HTTP client 29// TODO: MFA support
24
25// acr_values can be mfa or selective_mfa (mfa only for external users)
26// mfa amr values:
27// pas - password
28// otp - OTP code
29// u2f - U2F code
30// mfa - multi-factor
31// hrd - hardware OTP device used
32// sft - software OTP device used
33 30
34type ProxyConfig struct { 31type ProxyConfig struct {
35 IDProviderURL string 32 IdProviderURL *url.URL
36 ClientID string 33 IdProviderAuthEndpoint *url.URL
37 UpstreamURL string 34 ClientId *url.URL
38 ListenOn string 35 UpstreamURL string
39 TrustedCACert string 36 ListenOn string
40 PKISubject string // TODO: Should be same as IDP w/out scheme and port 37 TrustedCACert string
41 ClockSkew time.Duration 38 ClockSkew time.Duration
42 MaxLiftetime time.Duration 39 MaxLiftetime time.Duration
43 IsOptional bool 40 IsOptional bool
44 RequestMFA bool 41 IsBootstrap bool
45 AllowedMFAMethods []string // An OR set 42 RequestMFA bool
46 RequiredMFAMethods []string // An AND set 43 AllowedMFAMethods []string // An OR set
47 reverseProxy *httputil.ReverseProxy 44 RequiredMFAMethods []string // An AND set
45 reverseProxy *httputil.ReverseProxy
46 jwsValidator JWSValidator
48} 47}
49 48
50type IdPConfig struct { 49type IdPConfig struct {
51 AuthorizationEndpoint string `json:"authorization_endpoint"` 50 AuthorizationEndpoint *JSONURL `json:"authorization_endpoint"`
51 JwksUri *JSONURL `json:"jwks_uri"`
52 Issuer string `json:"issuer"` 52 Issuer string `json:"issuer"`
53 JwksUri string `json:"jwks_uri"`
54 GrantTypes []string `json:"grant_types_supported"` 53 GrantTypes []string `json:"grant_types_supported"`
55 IdTokenSigningAlgs []string `json:"id_token_signing_alg_values_supported"` 54 IdTokenSigningAlgs []string `json:"id_token_signing_alg_values_supported"`
56 ResponseModes []string `json:"response_modes_supported"` 55 ResponseModes []string `json:"response_modes_supported"`
@@ -59,258 +58,311 @@ type IdPConfig struct {
59 SubjectTypes []string `json:"subject_types_supported"` 58 SubjectTypes []string `json:"subject_types_supported"`
60} 59}
61 60
62// TODO: Optimization to fetch only if expired (per http headers) 61func FetchIdPConfig(h CautiousHTTPClient, u *url.URL) (*IdPConfig, error) {
63func FetchIdPConfig(h CautiousHTTPClient, idp_url string) (*IdPConfig, error) { 62 u = URLMustParse(u.String())
64 u, err := url.Parse(idp_url)
65 if err != nil {
66 return nil, err
67 }
68 u.Path = "/.well-known/openid-configuration" 63 u.Path = "/.well-known/openid-configuration"
69 64
70 var idpc IdPConfig 65 var idpc IdPConfig
71 err = h.GetJSON(u.String(), &idpc) 66 err := h.GetJSON(u.String(), &idpc)
72 if err != nil { 67 if err != nil {
73 return nil, err 68 return nil, errors.WithStack(err)
74 } 69 }
75 70
76 return &idpc, nil 71 return &idpc, nil
77} 72}
78 73
79// TODO: Optimization to fetch only if expired (per http headers)
80func FetchJWKS(h CautiousHTTPClient, jwks_url string, val KeyValidator) (map[string]jose.JSONWebKey, error) {
81 var jwks jose.JSONWebKeySet
82 err := h.GetJSON(jwks_url, &jwks)
83 if err != nil {
84 return nil, err
85 }
86
87 keys := make(map[string]jose.JSONWebKey, len(jwks.Keys))
88
89 for _, k := range jwks.Keys {
90 err = val.Validate(k)
91 if err == nil {
92 keys[k.KeyID] = k
93 }
94 }
95
96 return keys, nil
97}
98
99func GenerateNonce() (string, error) { 74func GenerateNonce() (string, error) {
100 nonce := make([]byte, NONCE_SIZE) 75 nonce := make([]byte, NONCE_SIZE)
101 n, err := rand.Read(nonce) 76 n, err := rand.Read(nonce)
102 if n != NONCE_SIZE || err != nil { 77 if n != NONCE_SIZE || err != nil {
103 return "", err 78 return "", errors.WithStack(err)
104 } 79 }
105 return hex.EncodeToString(nonce), nil 80 return hex.EncodeToString(nonce), nil
106} 81}
107 82
108// TODO 83func SetSecureCookie(w http.ResponseWriter, name string, value string, exp time.Duration) {
109// Cookie rules 84 http.SetCookie(w, &http.Cookie{
110// Secure 85 Name: name,
111// HttpOnly 86 Value: value,
112// Path to / 87 Expires: time.Now().Add(exp),
113// Expires to iat in JWT 88 HttpOnly: true,
114func SetCookie() { 89 Secure: true,
90 Path: "/",
91 })
115} 92}
116 93
117// TODO 94func ExpireCookie(w http.ResponseWriter, name string) {
118func MakeClientID(r *http.Request) string { 95 http.SetCookie(w, &http.Cookie{
119 if strings.Contains(r.Host, ":") { 96 Name: name,
120 return r.Host 97 Value: "",
121 } 98 Expires: time.Now().Add(-1 * time.Hour),
122 return "" 99 HttpOnly: true,
100 Secure: true,
101 Path: "/",
102 MaxAge: 0,
103 })
123} 104}
124 105
125// TODO 106func RedirectToIdP(w http.ResponseWriter, r *http.Request, path string) {
126func RedirectToIDP(w http.ResponseWriter, r *http.Request) { 107 ctx := r.Context().Value("ProxyConfig").(*ProxyConfig)
127 nonce, _ := GenerateNonce()
128 _ = nonce
129 nonceh := "" // SHA256 nonce
130 108
131 // Set nonce cookie 109 nonce, err := GenerateNonce()
110 if err != nil {
111 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
112 return
113 }
114
115 SetSecureCookie(w, RFP_COOKIE_NAME, nonce, DEFAULT_COOKIE_EXP)
116
117 rt := ""
118 rp := r.URL.Query().Get("redirect_uri")
119 if rp != "" {
120 rt = rp
121 } else {
122 ru := &url.URL{
123 Scheme: "https",
124 Host: r.Host,
125 Path: path,
126 }
127 rt = ru.String()
128 }
132 129
133 req := url.Values{} 130 req := url.Values{}
134 req.Add("client_id", "") // fqdn + : + port 131 req.Add("client_id", ctx.ClientId.String())
135 req.Add("nonce", nonceh) 132 req.Add("nonce", Sha256Hex(nonce))
136 req.Add("redirect_uri", "") // Requested URL 133 req.Add("redirect_uri", rt)
137 req.Add("scope", "openid") 134 req.Add("scope", "openid")
138 req.Add("response_type", "id_token") 135 req.Add("response_type", "id_token")
136
137 u := URLMustParse(ctx.IdProviderAuthEndpoint.String())
138 u.RawQuery = req.Encode()
139
140 http.Redirect(w, r, u.String(), http.StatusFound)
139} 141}
140 142
141// TODO: Remove id_token from URL, set cookie and redirect user to requested URL
142func SetTokenCookieAndRedirect(w http.ResponseWriter, r *http.Request, token string) { 143func SetTokenCookieAndRedirect(w http.ResponseWriter, r *http.Request, token string) {
143} 144 SetSecureCookie(w, TOKEN_COOKIE_NAME, token, DEFAULT_COOKIE_EXP)
144 145
145// TODO 146 q := r.URL.Query()
146func ValidateJWT(jwt, rfp string) bool { 147 q.Del("id_token")
147 return true 148
148} 149 u := URLMustParse(r.URL.String())
150 u.RawQuery = q.Encode()
149 151
150// TODO 152 http.Redirect(w, r, u.String(), http.StatusFound)
151func GetJWTSubject(jwt string) string {
152 return ""
153} 153}
154 154
155func RequestHasForwardedUser(w http.ResponseWriter, r *http.Request) bool { 155func RequestHasForwardedUser(w http.ResponseWriter, r *http.Request) bool {
156 if _, ok := r.Header["X-Forwarded-User"]; ok { 156 _, ok := r.Header["X-Forwarded-User"]
157 log.Printf("ERROR: Request contains X-Forwarded-For header") 157 return ok
158 http.Error(w, "Bad Request", http.StatusBadRequest)
159 return true
160 } else {
161 return false
162 }
163} 158}
164 159
165func RequestIsOverSecureChannel(w http.ResponseWriter, r *http.Request) bool { 160func RequestIsOverSecureChannel(w http.ResponseWriter, r *http.Request) bool {
166 https, ok := r.Header["X-Forwarded-Proto"] 161 https, ok := r.Header["X-Forwarded-Proto"]
167 if !ok || len(https) != 1 { 162 if !ok || len(https) != 1 {
168 log.Printf("ERROR: Request does not contain X-Forwarded-Proto header") 163 glog.V(1).Infoln("Request does not contain X-Forwarded-Proto header")
169 http.Error(w, "Bad Request", http.StatusBadRequest)
170 return false 164 return false
171 } 165 }
172 166
173 if !CompareUpper(https[0], "HTTPS") { 167 if !CompareUpper(https[0], "HTTPS") {
174 log.Printf("ERROR: Request is not over HTTPS") 168 glog.V(1).Infoln("Request is not over HTTPS")
175 http.Error(w, "Bad Request", http.StatusBadRequest)
176 return false 169 return false
177 } 170 }
178 171
179 return true 172 return true
180} 173}
181 174
182// TODO
183// - Validate Hostname header == known hostname
184func AuthProxyController(w http.ResponseWriter, r *http.Request) { 175func AuthProxyController(w http.ResponseWriter, r *http.Request) {
185 proxy := r.Context().Value("ProxyConfig").(*ProxyConfig).reverseProxy 176 ctx := r.Context().Value("ProxyConfig").(*ProxyConfig)
186 177
187 // Order matters in these checks! 178 // Order matters in these checks!
188 if RequestHasForwardedUser(w, r) { 179 if RequestHasForwardedUser(w, r) {
180 glog.Errorln("Request already has X-Forwarded-User")
181 http.Error(w, "Bad Request", http.StatusBadRequest)
189 return 182 return
190 } 183 }
191 184
192 if !RequestIsOverSecureChannel(w, r) { 185 if !RequestIsOverSecureChannel(w, r) {
186 http.Error(w, "Bad Request", http.StatusBadRequest)
193 return 187 return
194 } 188 }
195 189
196 if CompareUpper(r.Method, "OPTIONS") { 190 if CompareUpper(r.Method, "OPTIONS") {
197 proxy.ServeHTTP(w, r) 191 ctx.reverseProxy.ServeHTTP(w, r)
198 return 192 return
199 } 193 }
200 194
201 rfpc, err := r.Cookie(RFP_COOKIE_NAME) 195 rfpc, err := r.Cookie(RFP_COOKIE_NAME)
202 if err != nil { 196 if err != nil {
203 log.Printf("ERROR: No rfp cookie") 197 glog.V(1).Infoln("No rfp cookie")
204 RedirectToIDP(w, r) 198 if ctx.IsOptional {
205 return 199 ctx.reverseProxy.ServeHTTP(w, r)
200 return
201 } else {
202 RedirectToIdP(w, r, r.URL.Path)
203 return
204 }
206 } 205 }
207 206
208 token := r.URL.Query().Get("id_token") 207 if token := r.URL.Query().Get("id_token"); token != "" {
209 if token != "" && ValidateJWT(rfpc.Value, token) { 208 if _, err := ctx.jwsValidator.Validate(token, rfpc.Value); err == nil {
210 SetTokenCookieAndRedirect(w, r, token) 209 SetTokenCookieAndRedirect(w, r, token)
211 return 210 return
211 } else {
212 glog.V(1).Infof("Querystring id_token invalid: %s", err)
213 }
212 } 214 }
213 215
214 tokenc, err := r.Cookie(TOKEN_COOKIE_NAME) 216 tokenc, err := r.Cookie(TOKEN_COOKIE_NAME)
215 if err != nil { 217 if err != nil {
216 log.Printf("ERROR: No token cookie") 218 glog.V(1).Infoln("No token cookie")
217 RedirectToIDP(w, r) 219 if ctx.IsOptional {
218 return 220 ctx.reverseProxy.ServeHTTP(w, r)
221 return
222 } else {
223 RedirectToIdP(w, r, r.URL.Path)
224 return
225 }
219 } 226 }
220 227
221 if !ValidateJWT(tokenc.Value, rfpc.Value) { 228 claims, err := ctx.jwsValidator.Validate(tokenc.Value, rfpc.Value)
222 log.Printf("ERROR: Token is invalid") 229 if err != nil {
223 RedirectToIDP(w, r) 230 glog.Errorln("Token is invalid", err)
224 return 231 if ctx.IsOptional {
232 ctx.reverseProxy.ServeHTTP(w, r)
233 return
234 } else {
235 RedirectToIdP(w, r, r.URL.Path)
236 return
237 }
225 } 238 }
226 239
227 r.Header["X-Forwarded-User"] = []string{GetJWTSubject(tokenc.Value)} 240 r.Header["X-Forwarded-User"] = []string{claims.Subject}
241 r.Header["X-Forwarded-Token-Expires"] = []string{strconv.FormatInt(int64(claims.Expiry), 10)}
242
243 age := time.Since(claims.IssuedAt.Time()).Minutes()
244 r.Header["X-Forwarded-Token-Age"] = []string{strconv.FormatInt(int64(age), 10)}
228 245
229 proxy.ServeHTTP(w, r) 246 ctx.reverseProxy.ServeHTTP(w, r)
230} 247}
231 248
232// Remove token and rfp cookies and redirect user to root of domain
233func LogoutController(w http.ResponseWriter, r *http.Request) { 249func LogoutController(w http.ResponseWriter, r *http.Request) {
234 http.SetCookie(w, &http.Cookie{ 250 ExpireCookie(w, RFP_COOKIE_NAME)
235 Name: TOKEN_COOKIE_NAME, 251 ExpireCookie(w, TOKEN_COOKIE_NAME)
236 Value: "",
237 MaxAge: 0,
238 })
239
240 http.SetCookie(w, &http.Cookie{
241 Name: TOKEN_COOKIE_NAME,
242 Value: "",
243 MaxAge: 0,
244 })
245
246 http.Redirect(w, r, "/", http.StatusFound) 252 http.Redirect(w, r, "/", http.StatusFound)
247} 253}
248 254
249// TODO
250func LoginController(w http.ResponseWriter, r *http.Request) {
251}
252
253// TODO
254// Optional login allows for applications that can operate in anonymous mode or 255// Optional login allows for applications that can operate in anonymous mode or
255// authenticated mode. When in anonmyous mode the request is proxied through 256// authenticated mode. When in anonmyous mode the request is proxied through
256// without an X-Forwarded-User header. Upstream servers should either expose or 257// without an X-Forwarded-User header. Upstream servers should either expose or
257// map a URL for /.oidc/login to allow users to login. On successful login the 258// map a URL for /.oidc/login to allow users to login. On successful login the
258// user will be redirected back to the main page for the site (/) 259// user will be redirected back to the main page for the site (/)
259func parseConfig() *ProxyConfig { 260func parseConfig() (*ProxyConfig, error) {
260 return &ProxyConfig{ 261 c := &ProxyConfig{}
261 IDProviderURL: "http://mcrute-virt:9993", 262
262 ClientID: "test.crute.me:443", 263 idpu := flag.String("idp", "", "URL for ID provider")
263 UpstreamURL: "http://localhost:9991/", 264 mfam := flag.String("allow-mfa-methods", "", "Comma seperated list of allowed mfa methods")
264 ListenOn: ":9992", 265 rmfa := flag.String("require-mfa-methods", "", "Comma seperated list of required mfa methods")
265 TrustedCACert: "/home/mcrute/oidc_project/test_ca/ca_cert.pem", 266 cids := flag.String("client-id", "", "Client ID for proxy with IdP")
266 IsOptional: false, 267
267 PKISubject: "Crute OpenID Signing 1", 268 flag.BoolVar(&c.IsOptional, "optional", false, "Allow proxying of unauthenticated calls")
268 MaxLiftetime: 24 * time.Hour, 269 flag.BoolVar(&c.IsBootstrap, "bootstrap", false, "Allow running a proxy for the IdP itself")
269 ClockSkew: 5 * time.Minute, 270 flag.BoolVar(&c.RequestMFA, "mfa", false, "Request user MFA authentication from IdP")
271
272 flag.DurationVar(&c.MaxLiftetime, "max-lifetime", DEFAULT_MAX_LIFETIME, "Maximum allowed time from token issuance")
273 flag.DurationVar(&c.ClockSkew, "clock-skew", DEFAULT_CLOCK_SKEW, "Allowable IdP clock skew relative to proxy")
274
275 flag.StringVar(&c.UpstreamURL, "upstream", "", "URL of upstream service for which to proxy")
276 flag.StringVar(&c.ListenOn, "listen", ":9992", "Optional port and ip on which to listen")
277 flag.StringVar(&c.TrustedCACert, "ca", "", "Path to trusted CA certificate")
278
279 flag.Parse()
280
281 c.AllowedMFAMethods = strings.Split(*mfam, ",")
282 c.RequiredMFAMethods = strings.Split(*rmfa, ",")
283
284 if c.IsBootstrap {
285 c.IsOptional = true
286 }
287
288 if _, err := os.Stat(c.TrustedCACert); os.IsNotExist(err) {
289 return nil, errors.Errorf("CA certificate does not exist")
290 }
291
292 if cids == nil {
293 return nil, errors.Errorf("Client ID is required")
294 }
295
296 if client_id, err := url.Parse(*cids); err != nil || client_id.Host == "" {
297 return nil, errors.Errorf("Invalid client ID")
298 } else {
299 c.ClientId = client_id
300 }
301
302 if c.UpstreamURL == "" {
303 return nil, errors.Errorf("Upstream URL is required")
304 }
305
306 if idpu == nil {
307 return nil, errors.Errorf("IDP url is required")
270 } 308 }
309
310 if u, err := url.Parse(*idpu); err != nil {
311 return nil, errors.WithStack(err)
312 } else {
313 c.IdProviderURL = u
314
315 if h := HostFromURL(u.String()); c.IsBootstrap && (h != "localhost" && h != "127.0.0.1") {
316 return nil, errors.Errorf("IdP must be set to localhost for bootstrap")
317 }
318 }
319
320 return c, nil
271} 321}
272 322
273func main() { 323func main() {
274 cfg := parseConfig() 324 cfg, err := parseConfig()
275 h := NewCautiousHTTPClient() 325 if err != nil {
276 326 glog.Fatalln("ParseConfig", err)
277 v := NewKeyValidator(cfg.PKISubject) 327 return
278 v.LoadRootPEM(cfg.TrustedCACert) 328 }
279 329
280 idpc, err := FetchIdPConfig(h, cfg.IDProviderURL) 330 hidp, err := NewCautiousHTTPClient(cfg.IsBootstrap)
281 if err != nil { 331 if err != nil {
282 fmt.Printf("%s\n", err) 332 glog.Fatalln("Error building http client", err)
283 return 333 return
284 } 334 }
285 335
286 jwks, err := FetchJWKS(h, idpc.JwksUri, v) 336 idpc, err := FetchIdPConfig(hidp, cfg.IdProviderURL)
287 if err != nil { 337 if err != nil {
288 fmt.Printf("%s\n", err) 338 glog.Fatalln("FetchIdPConfig:", err)
289 return 339 return
290 } 340 }
291 341
292 jv := NewJWSValidator(jwks, idpc.Issuer, cfg.ClientID, cfg.ClockSkew, cfg.MaxLiftetime) 342 cfg.IdProviderAuthEndpoint = idpc.AuthorizationEndpoint.AsURL()
293 343
294 nonce := "ofspmfjuvoswhhde" 344 h, err := NewCautiousHTTPClient(false)
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 { 345 if err != nil {
298 fmt.Printf("Error validating: %s\n", err) 346 glog.Fatalln("Error building http client", err)
299 return 347 return
300 } 348 }
301 349
302 fmt.Printf("Valid JWT for: %+v\n", claims.Subject) 350 kf := NewJWKSFetcher(h, idpc.JwksUri.AsURL(), idpc.Issuer, cfg.TrustedCACert)
303 351
304 return 352 cfg.jwsValidator = NewJWSValidator(&JWSValidationContext{
353 KeyFetcher: kf,
354 Issuer: idpc.Issuer,
355 ClientId: cfg.ClientId,
356 ClockSkew: cfg.ClockSkew,
357 MaxLiftetime: cfg.MaxLiftetime,
358 })
305 359
306 cfg.reverseProxy = httputil.NewSingleHostReverseProxy(URLMustParse(cfg.UpstreamURL)) 360 cfg.reverseProxy = httputil.NewSingleHostReverseProxy(URLMustParse(cfg.UpstreamURL))
307 361
308 if cfg.IsOptional { 362 http.HandleFunc("/.oidc/login", func(w http.ResponseWriter, r *http.Request) {
309 http.HandleFunc("/.oidc/login", func(w http.ResponseWriter, r *http.Request) { 363 RedirectToIdP(w,
310 LoginController(w, 364 r.WithContext(context.WithValue(r.Context(), "ProxyConfig", cfg)), "/")
311 r.WithContext(context.WithValue(r.Context(), "ProxyConfig", cfg))) 365 })
312 })
313 }
314 366
315 http.HandleFunc("/.oidc/logout", func(w http.ResponseWriter, r *http.Request) { 367 http.HandleFunc("/.oidc/logout", func(w http.ResponseWriter, r *http.Request) {
316 LogoutController(w, 368 LogoutController(w,
@@ -322,5 +374,14 @@ func main() {
322 r.WithContext(context.WithValue(r.Context(), "ProxyConfig", cfg))) 374 r.WithContext(context.WithValue(r.Context(), "ProxyConfig", cfg)))
323 }) 375 })
324 376
325 log.Fatal(http.ListenAndServe(cfg.ListenOn, nil)) 377 go http.ListenAndServe(cfg.ListenOn,
378 handlers.LoggingHandler(os.Stdout, http.DefaultServeMux))
379
380 // This has to happen last in-case we're boostrapping a proxy for the IdP itself
381 if err := kf.Fetch(); err != nil {
382 glog.Fatalln("FetchJWKS:", err)
383 return
384 } else {
385 kf.Run()
386 }
326} 387}