summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2017-08-25 01:09:39 +0000
committerMike Crute <mike@crute.us>2017-08-25 01:09:39 +0000
commitb602fac5decc5daab15af6abf2dc6dfeb649c1d5 (patch)
tree88c677c9dd56d0150b127a8c3d4e641484a44329
downloadoidc_proxy-b602fac5decc5daab15af6abf2dc6dfeb649c1d5.tar.bz2
oidc_proxy-b602fac5decc5daab15af6abf2dc6dfeb649c1d5.tar.xz
oidc_proxy-b602fac5decc5daab15af6abf2dc6dfeb649c1d5.zip
Initial stub checkin
-rw-r--r--main.go263
1 files changed, 263 insertions, 0 deletions
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..a96f0eb
--- /dev/null
+++ b/main.go
@@ -0,0 +1,263 @@
1package main
2
3import (
4 "context"
5 "crypto/rand"
6 "encoding/hex"
7 _ "github.com/dgrijalva/jwt-go"
8 "log"
9 "net/http"
10 "net/http/httputil"
11 "net/url"
12 "strings"
13)
14
15const (
16 NONCE_SIZE int = 16
17 TOKEN_COOKIE_NAME string = "sso_token"
18 RFP_COOKIE_NAME string = "sso_rfp"
19)
20
21// acr_values can be mfa or selective_mfa (mfa only for external users)
22// mfa amr values:
23// pas - password
24// otp - OTP code
25// u2f - U2F code
26// mfa - multi-factor
27// hrd - hardware OTP device used
28// sft - software OTP device used
29
30type ProxyConfig struct {
31 IDProviderURL string
32 ClientID string
33 UpstreamURL string
34 ListenOn string
35 TrustedCACert string
36 RequestMFA bool
37 AllowedMFAMethods []string // An OR set
38 RequiredMFAMethods []string // An AND set
39 reverseProxy *httputil.ReverseProxy
40}
41
42func URLMustParse(u string) *url.URL {
43 o, err := url.Parse(u)
44 if err != nil {
45 panic(err)
46 }
47 return o
48}
49
50func GenerateNonce() (string, error) {
51 nonce := make([]byte, NONCE_SIZE)
52 n, err := rand.Read(nonce)
53 if n != NONCE_SIZE || err != nil {
54 return "", err
55 }
56 return hex.EncodeToString(nonce), nil
57}
58
59func CompareUpper(lhs, rhs string) bool {
60 return strings.ToUpper(lhs) == strings.ToUpper(rhs)
61}
62
63// TODO
64// Cookie rules
65// Secure
66// HttpOnly
67// Path to /
68// Expires to iat in JWT
69func SetCookie() {
70}
71
72// TODO
73// Fetch (connect timeout 1s, read timeout 30s, read size 1M)
74func DownloadCertificate() {
75}
76
77// TODO
78// Fetch (connect timeout 1s, read timeout 30s, read size 1M)
79func DownloadCRL() {
80}
81
82// TODO
83// Cert validation
84// Validate cert not in CRL
85// Validate cert chains to trusted CA cert (ship with proxy)
86// Validate CRL signed by trusted CA
87// Cert "Subject CN " must match exactly PKI setting (ex: foo-pki.foo.com)
88// Current time bust be within "Validity Not Before" and "Validity Not After" in cert +- 5 minutes
89// Cert Key length >= 2048
90// Certificate usage must include "digitalSignature"
91func ValidateCertificate() {
92}
93
94func MakeClientID(r *http.Request) string {
95 if strings.Contains(r.Host, ":") {
96 return r.Host
97 }
98}
99
100// TODO
101func RedirectToIDP(w http.ResponseWriter, r *http.Request) {
102 nonce := GenerateNonce()
103 nonceh := "" // SHA256 nonce
104
105 // Set nonce cookie
106
107 req := url.Values{}
108 req.Add("client_id", "") // fqdn + : + port
109 req.Add("nonce", nonceh)
110 req.Add("redirect_uri", "") // Requested URL
111 req.Add("scope", "openid")
112 req.Add("response_type", "id_token")
113}
114
115// TODO: Remove id_token from URL, set cookie and redirect user to requested URL
116func SetTokenCookieAndRedirect(w http.ResponseWriter, r *http.Request, token string) {
117}
118
119// TODO
120// Fetch cert from x5u URL
121// Get CRL from cert, fetch (connect timeout 1s, read timeout 30s, read size 1M)
122//
123// exp claim has passed +- 5 minutes
124// iat claim is greater than 24 hours +- 5 minutes
125// aud claim is exact match for client_id
126// iss claim is exact match for idp (ex: foo.example.com
127// if other aud claims validate that they are known
128// nonce in JWT must be SHA256 of rfp cookie value
129// Validate cert
130// alg jwt header must be one of [PS256, PS385, PS512]
131// typ jwt header must be JWS
132// validate jwt signature
133// validate amr claim contains requested acr values (selective_mfa will be just mfa)
134// validate acr claim is the same as requested acr_values
135func ValidateJWT(jwt, rfp string) bool {
136 return true
137}
138
139// TODO
140func GetJWTSubject(jwt string) string {
141 return ""
142}
143
144func RequestHasForwardedUser(w http.ResponseWriter, r *http.Request) bool {
145 if _, ok := r.Header["X-Forwarded-User"]; ok {
146 log.Printf("ERROR: Request contains X-Forwarded-For header")
147 http.Error(w, "Bad Request", http.StatusBadRequest)
148 return true
149 } else {
150 return false
151 }
152}
153
154func RequestIsOverSecureChannel(w http.ResponseWriter, r *http.Request) {
155 if https, ok := r.Header["X-Forwarded-Proto"]; !ok || len(https) != 1 {
156 log.Printf("ERROR: Request does not contain X-Forwarded-Proto header")
157 http.Error(w, "Bad Request", http.StatusBadRequest)
158 return false
159 }
160
161 if !CompareUpper(https[0], "HTTPS") {
162 log.Printf("ERROR: Request is not over HTTPS")
163 http.Error(w, "Bad Request", http.StatusBadRequest)
164 return false
165 }
166
167 return true
168}
169
170// TODO
171// - Validate Hostname header == known hostname
172func AuthProxyController(w http.ResponseWriter, r *http.Request) {
173 proxy := r.Context().Value("ProxyConfig").(*ProxyConfig).reverseProxy
174
175 // Order matters in these checks!
176 if RequestHasForwardedUser(w, r) {
177 return
178 }
179
180 if !RequestIsOverSecureChannel(w, r) {
181 return
182 }
183
184 if CompareUpper(r.Method, "OPTIONS") {
185 proxy.ServeHTTP(w, r)
186 return
187 }
188
189 rfpc, err := r.Cookie(RFP_COOKIE_NAME)
190 if err != nil {
191 log.Printf("ERROR: No rfp cookie")
192 RedirectToIDP(w, r)
193 return
194 }
195
196 token := r.URL.Query().Get("id_token")
197 if token != "" && ValidateJWT(rfpc.Value, token) {
198 SetTokenCookieAndRedirect(w, r, token)
199 return
200 }
201
202 tokenc, err := r.Cookie(TOKEN_COOKIE_NAME)
203 if err != nil {
204 log.Printf("ERROR: No token cookie")
205 RedirectToIDP()
206 return
207 }
208
209 if !ValidateJWT(tokenc.Value, rfpc.Value) {
210 log.Printf("ERROR: Token is invalid")
211 RedirectToIDP()
212 return
213 }
214
215 r.Header["X-Forwarded-User"] = []string{GetJWTSubject(tokenc.Value)}
216
217 proxy.ServeHTTP(w, r)
218}
219
220// Remove token and rfp cookies and redirect user to root of domain
221func LogoutController(w http.ResponseWriter, r *http.Request) {
222 http.SetCookie(w, &http.Cookie{
223 Name: TOKEN_COOKIE_NAME,
224 Value: "",
225 MaxAge: 0,
226 })
227
228 http.SetCookie(w, &http.Cookie{
229 Name: TOKEN_COOKIE_NAME,
230 Value: "",
231 MaxAge: 0,
232 })
233
234 http.Redirect(w, r, "/", http.StatusFound)
235}
236
237// TODO
238func parseConfig() *ProxyConfig {
239 return &ProxyConfig{
240 IDProviderURL: "",
241 ClientID: "",
242 UpstreamURL: "http://localhost:9991/",
243 ListenOn: ":9992",
244 TrustedCACert: "",
245 }
246}
247
248func main() {
249 cfg := parseConfig()
250 cfg.reverseProxy = httputil.NewSingleHostReverseProxy(URLMustParse(cfg.UpstreamURL))
251
252 http.HandleFunc("/.oidc/logout", func(w http.ResponseWriter, r *http.Request) {
253 LogoutController(w,
254 r.WithContext(context.WithValue(r.Context(), "ProxyConfig", cfg)))
255 })
256
257 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
258 AuthProxyController(w,
259 r.WithContext(context.WithValue(r.Context(), "ProxyConfig", cfg)))
260 })
261
262 log.Fatal(http.ListenAndServe(cfg.ListenOn, nil))
263}