-
Notifications
You must be signed in to change notification settings - Fork 1
/
security.go
325 lines (276 loc) · 9.1 KB
/
security.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
package main
import (
"context"
"encoding/gob"
"fmt"
"log"
"net/http"
"net/url"
"strings"
"time"
"github.com/coreos/go-oidc"
"github.com/gorilla/sessions"
"golang.org/x/oauth2"
)
type SecurityFrontend struct {
OpenId OpenIdSettings
Protected http.Handler
TokenGenerator *TokenGenerator
GlobalTokenValidity int
PerAlbumTokenValidity int
store *sessions.CookieStore
oAuth2Config *oauth2.Config
oidcVerifier *oidc.IDTokenVerifier
}
type SessionSettings struct {
AuthenticationKey []byte
EncryptionKey []byte
CookieMaxAge int
SecureCookie bool
}
type OpenIdSettings struct {
DiscoveryUrl string
ClientID string
ClientSecret string
RedirectURL string
GSuiteDomain string
Scopes []string
}
func init() {
gob.Register(&WebUser{})
}
func GetOAuthCallbackURL(publicUrl string) string {
u, err := url.Parse(publicUrl)
if err != nil {
// If the URL cannot be parsed, use it as-is
return publicUrl
}
u.Path = "/oauth/callback"
u.Fragment = ""
u.RawQuery = ""
return u.String()
}
func NewSecurityFrontend(openidSettings OpenIdSettings, sessionSettings SessionSettings, tokenGenerator *TokenGenerator) (*SecurityFrontend, error) {
var securityFrontend SecurityFrontend
provider, err := oidc.NewProvider(context.TODO(), openidSettings.DiscoveryUrl)
if err != nil {
return nil, err
}
securityFrontend.oAuth2Config = &oauth2.Config{
ClientID: openidSettings.ClientID,
ClientSecret: openidSettings.ClientSecret,
RedirectURL: openidSettings.RedirectURL,
// Discovery returns the OAuth2 endpoints.
Endpoint: provider.Endpoint(),
// "openid" is a required scope for OpenID Connect flows.
Scopes: append(openidSettings.Scopes, oidc.ScopeOpenID),
}
securityFrontend.oidcVerifier = provider.Verifier(&oidc.Config{ClientID: openidSettings.ClientID})
securityFrontend.store = sessions.NewCookieStore(sessionSettings.AuthenticationKey, sessionSettings.EncryptionKey)
securityFrontend.store.Options = &sessions.Options{
Path: "/",
MaxAge: sessionSettings.CookieMaxAge,
HttpOnly: true,
Secure: sessionSettings.SecureCookie,
}
securityFrontend.OpenId = openidSettings
securityFrontend.TokenGenerator = tokenGenerator
return &securityFrontend, nil
}
func (securityFrontend *SecurityFrontend) ServeHTTP(w http.ResponseWriter, r *http.Request) {
originalPath := r.URL.Path
if r.URL.Path == "/oauth/callback" {
if r.Method != "GET" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
securityFrontend.handleOidcCallback(w, r)
return
}
head, tail := ShiftPath(r.URL.Path)
var user *WebUser
if head == "s" {
var ok bool
r.URL.Path = tail
user, ok = securityFrontend.handleTelegramTokenAuthentication(w, r)
if !ok {
return
}
} else if head == "album" {
var ok bool
user, ok = securityFrontend.handleOidcAuthentication(w, r)
if !ok {
return
}
} else {
user = &WebUser{}
}
log.Printf("[%s] %s %s", user, r.Method, r.URL.Path)
// Respect the user's choice about trailing slash
if strings.HasSuffix(originalPath, "/") && !strings.HasSuffix(r.URL.Path, "/") {
r.URL.Path = r.URL.Path + "/"
}
securityFrontend.Protected.ServeHTTP(w, r)
}
func (securityFrontend *SecurityFrontend) handleOidcRedirect(w http.ResponseWriter, r *http.Request, session *sessions.Session, forcedTargetPath string) {
nonce, err := newRandomSecret(32)
if err != nil {
log.Printf("rand.Read: %s", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
state, err := newRandomSecret(32)
if err != nil {
log.Printf("rand.Read: %s", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
session.AddFlash(nonce.String())
session.AddFlash(state.String())
if forcedTargetPath != "" {
session.AddFlash(forcedTargetPath)
} else {
session.AddFlash(r.URL.Path)
}
err = session.Save(r, w)
if err != nil {
log.Printf("Session.Save: %s", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
http.Redirect(w, r, securityFrontend.oAuth2Config.AuthCodeURL(state.Hashed(), oidc.Nonce(nonce.Hashed())), http.StatusFound)
}
func (securityFrontend *SecurityFrontend) handleOidcCallback(w http.ResponseWriter, r *http.Request) {
session, err := securityFrontend.store.Get(r, "oidc")
if err != nil {
log.Printf("session.Store.Get: %s", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Retrieve the nonce and state from the session flashes
nonceAndState := session.Flashes()
if len(nonceAndState) < 3 { // there may be more than two if the user performs multiple attempts
log.Printf("session.Flashes: no (nonce,state,redirect_path) found in current session (len = %d)", len(nonceAndState))
securityFrontend.handleOidcRedirect(w, r, session, "/")
return
}
nonce, err := secretFromHex(nonceAndState[len(nonceAndState)-3].(string))
if err != nil {
log.Printf("hex.DecodeString: %s", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
state, err := secretFromHex(nonceAndState[len(nonceAndState)-2].(string))
if err != nil {
log.Printf("hex.DecodeString: %s", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
redirect_path := nonceAndState[len(nonceAndState)-1].(string)
if redirect_path == "" {
redirect_path = "/"
}
if r.URL.Query().Get("state") != state.Hashed() {
log.Println("OIDC callback: state do not match")
http.Error(w, "state does not match", http.StatusBadRequest)
return
}
oauth2Token, err := securityFrontend.oAuth2Config.Exchange(context.TODO(), r.URL.Query().Get("code"))
if err != nil {
log.Printf("oauth2.Config.Exchange: %s", err)
http.Error(w, "Invalid Authorization Code", http.StatusBadRequest)
return
}
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
log.Println("Token.Extra: No id_token field in oauth2 token")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
user, err := securityFrontend.validateIdToken(rawIDToken, nonce.Hashed())
if err != nil {
log.Printf("validateIdToken: %s", err)
//log.Printf("invalid id_token: %s", rawIDToken)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
log.Printf("HTTP: user %s logged in", user.Username)
session.Values["user"] = &user
err = session.Save(r, w)
if err != nil {
log.Printf("Session.Save: %s", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
http.Redirect(w, r, redirect_path, http.StatusFound)
}
func (securityFrontend *SecurityFrontend) validateIdToken(rawIDToken string, nonce string) (WebUser, error) {
idToken, err := securityFrontend.oidcVerifier.Verify(context.TODO(), rawIDToken)
if err != nil {
return WebUser{}, fmt.Errorf("IDTokenVerifier.Verify: %s", err)
}
if idToken.Nonce != nonce {
return WebUser{}, fmt.Errorf("nonces do not match in id_token")
}
var claims struct {
Email string `json:"email"`
GSuiteDomain string `json:"hd"`
}
err = idToken.Claims(&claims)
if err != nil {
return WebUser{}, fmt.Errorf("IdToken.Claims: %s", err)
}
if securityFrontend.OpenId.GSuiteDomain != "" && securityFrontend.OpenId.GSuiteDomain != claims.GSuiteDomain {
return WebUser{}, fmt.Errorf("GSuite domain '%s' is not allowed", claims.GSuiteDomain)
}
return WebUser{Username: claims.Email, Type: TypeOidcUser}, nil
}
func (securityFrontend *SecurityFrontend) handleOidcAuthentication(w http.ResponseWriter, r *http.Request) (*WebUser, bool) {
session, err := securityFrontend.store.Get(r, "oidc")
if err != nil {
log.Printf("session.Store.Get: %s", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return &WebUser{}, false
}
u := session.Values["user"]
if u == nil {
securityFrontend.handleOidcRedirect(w, r, session, "")
return &WebUser{}, false
}
user, ok := u.(*WebUser)
if !ok {
log.Println("Cannot cast session item 'user' as WebUser")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return &WebUser{}, false
}
return user, true
}
func (securityFrontend *SecurityFrontend) handleTelegramTokenAuthentication(w http.ResponseWriter, r *http.Request) (*WebUser, bool) {
var username, token string
username, r.URL.Path = ShiftPath(r.URL.Path)
token, r.URL.Path = ShiftPath(r.URL.Path)
var tail string
_, tail = ShiftPath(r.URL.Path)
album, _ := ShiftPath(tail)
data := TokenData{
Username: username,
Timestamp: time.Now(),
Entitlement: album,
}
// try to validate the token with an album entitlement
ok, err := securityFrontend.TokenGenerator.ValidateToken(data, token, securityFrontend.PerAlbumTokenValidity)
if err != nil {
http.Error(w, "Invalid Token", http.StatusBadRequest)
return nil, false
}
if !ok {
// if it fails, it may be a global token
data.Entitlement = ""
ok, err := securityFrontend.TokenGenerator.ValidateToken(data, token, securityFrontend.GlobalTokenValidity)
if !ok || err != nil {
http.Error(w, "Invalid Token", http.StatusBadRequest)
return nil, false
}
}
return &WebUser{Username: username, Type: TypeTelegramUser}, true
}