diff options
Diffstat (limited to 'main.go')
-rw-r--r-- | main.go | 409 |
1 files changed, 235 insertions, 174 deletions
@@ -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 | ||
17 | const ( | 20 | const ( |
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 | ||
34 | type ProxyConfig struct { | 31 | type 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 | ||
50 | type IdPConfig struct { | 49 | type 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) | 61 | func FetchIdPConfig(h CautiousHTTPClient, u *url.URL) (*IdPConfig, error) { |
63 | func 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) | ||
80 | func 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 | |||
99 | func GenerateNonce() (string, error) { | 74 | func 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 | 83 | func 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, |
114 | func SetCookie() { | 89 | Secure: true, |
90 | Path: "/", | ||
91 | }) | ||
115 | } | 92 | } |
116 | 93 | ||
117 | // TODO | 94 | func ExpireCookie(w http.ResponseWriter, name string) { |
118 | func 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 | 106 | func RedirectToIdP(w http.ResponseWriter, r *http.Request, path string) { |
126 | func 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 | ||
142 | func SetTokenCookieAndRedirect(w http.ResponseWriter, r *http.Request, token string) { | 143 | func 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() |
146 | func 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) |
151 | func GetJWTSubject(jwt string) string { | ||
152 | return "" | ||
153 | } | 153 | } |
154 | 154 | ||
155 | func RequestHasForwardedUser(w http.ResponseWriter, r *http.Request) bool { | 155 | func 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 | ||
165 | func RequestIsOverSecureChannel(w http.ResponseWriter, r *http.Request) bool { | 160 | func 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 | ||
184 | func AuthProxyController(w http.ResponseWriter, r *http.Request) { | 175 | func 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 | ||
233 | func LogoutController(w http.ResponseWriter, r *http.Request) { | 249 | func 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 | ||
250 | func 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 (/) |
259 | func parseConfig() *ProxyConfig { | 260 | func 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 | ||
273 | func main() { | 323 | func 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 | } |