summaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'main.go')
-rw-r--r--main.go119
1 files changed, 112 insertions, 7 deletions
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)))