This repository has been archived by the owner on Oct 23, 2024. It is now read-only.
forked from arrikto/oidc-authservice
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsession.go
195 lines (175 loc) · 6.17 KB
/
session.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
package main
import (
"context"
"net/http"
"os"
"github.com/boltdb/bolt"
"github.com/coreos/go-oidc"
"github.com/emirpasic/gods/sets/hashset"
"github.com/gorilla/sessions"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/yosssi/boltstore/reaper"
boltstore "github.com/yosssi/boltstore/store"
"golang.org/x/oauth2"
)
const (
userSessionCookie = "authservice_session"
userSessionUserID = "userid"
userSessionGroups = "groups"
userSessionClaims = "claims"
userSessionIDToken = "idtoken"
userSessionOAuth2Tokens = "oauth2tokens"
)
// sessionFromRequestHeader returns a session which has its key in a header.
// XXX: Because the session library we use doesn't support getting a session
// by key, we need to fake a cookie
func sessionFromID(id string, store sessions.Store) (*sessions.Session, error) {
r := &http.Request{Header: make(http.Header)}
r.AddCookie(&http.Cookie{
// XXX: This is needed because the sessions lib we use also encodes
// cookies with securecookie, which requires passing the correct cookie
// name during decryption.
Name: userSessionCookie,
Value: id,
Path: "/",
MaxAge: 1,
})
return store.Get(r, userSessionCookie)
}
// sessionFromRequest looks for a session id in a header and a cookie, in that
// order. If it doesn't find a valid session in the header, it will then check
// the cookie.
func sessionFromRequest(r *http.Request, store sessions.Store, cookie,
header string) (*sessions.Session, string, error) {
var authMethod string
logger := loggerForRequest(r, "session authenticator")
// Try to get session from header
sessionID := getBearerToken(r.Header.Get(header))
if sessionID != "" {
s, err := sessionFromID(sessionID, store)
if err == nil && !s.IsNew {
logger.Infof("Loading session from header %s", header)
// Authentication using header successfully completed
authMethod = "header"
return s, authMethod, nil
}
logger.Infof("Header %s didn't contain a valid session id: %v", header, err)
}
// Header failed, try to get session from cookie
logger.Infof("Loading session from cookie %s", cookie)
s, err := store.Get(r, cookie)
if err == nil && !s.IsNew {
authMethod = "cookie"
}
return s, authMethod, err
}
// revokeSession revokes the given session.
func revokeSession(ctx context.Context, w http.ResponseWriter,
session *sessions.Session) error {
// Delete the session by setting its MaxAge to a negative number.
// This will delete the session from the store and also add a "Set-Cookie"
// header that will instruct the browser to delete it.
// XXX: The session.Save function doesn't really need the request, but only
// uses it for its context.
session.Options.MaxAge = -1
r := &http.Request{}
if err := session.Save(r.WithContext(ctx), w); err != nil {
return errors.Wrap(err, "Couldn't delete user session")
}
return nil
}
// revokeOIDCSession revokes the given session, which is assumed to be an OIDC
// session, for which it also performs the necessary cleanup.
// TODO: In the future, we may want to make this function take a function as
// input, instead of polluting it with extra arguments.
func revokeOIDCSession(ctx context.Context, w http.ResponseWriter,
session *sessions.Session, provider *oidc.Provider,
oauth2Config *oauth2.Config, caBundle []byte) error {
logger := logrus.StandardLogger()
// Revoke the session's OAuth tokens
_revocationEndpoint, err := revocationEndpoint(provider)
if err != nil {
logger.Warnf("Error getting provider's revocation_endpoint: %v", err)
} else {
token := session.Values[userSessionOAuth2Tokens].(oauth2.Token)
err := revokeTokens(setTLSContext(ctx, caBundle), _revocationEndpoint,
&token, oauth2Config.ClientID, oauth2Config.ClientSecret)
if err != nil {
return errors.Wrap(err, "Error revoking tokens")
}
logger.WithField("userid", session.Values[userSessionUserID].(string)).Info("Access/Refresh tokens revoked")
}
return revokeSession(ctx, w, session)
}
type boltDBSessionStore struct {
sessions.Store
// DB is the underlying BoltDB instance.
DB *bolt.DB
// Channels for BoltDB reaper
// quitC sends the quit signal to the reaper goroutine.
// doneC receives the signal that the reaper has quit.
quitC chan<- struct{}
doneC <-chan struct{}
}
type existingDBEntry struct {
DB *bolt.DB
buckets *hashset.Set
}
var existingDBs = map[string]*existingDBEntry{}
// newBoltDBSessionStore returns a session store backed by BoltDB. The database
// is stored in the given path and keys are stored in the given bucket. If the
// path has been used before, it can reuses the same database if the
// allowDBReuse option is true.
// Returns the session store and a storeCloser interface, which should be called
// before shutting down in order to perform cleanup.
func newBoltDBSessionStore(path, bucket string, allowDBReuse bool) (*boltDBSessionStore, error) {
// Get realpath if the file already exists
_, err := os.Stat(path)
if !os.IsNotExist(err) {
path, err = realpath(path)
if err != nil {
return nil, err
}
}
// Retrieve existing DB or create new one
var db *bolt.DB
if existingDB, ok := existingDBs[path]; ok {
if !allowDBReuse {
return nil, errors.New("BoltDB instance is already used and allowDBReuse is false")
}
if existingDB.buckets.Contains(bucket) {
return nil, errors.New("BoltDB instance already has a bucket " +
"the same name used by another session store")
}
db = existingDB.DB
} else {
db, err = bolt.Open(path, 0600, nil)
if err != nil {
return nil, err
}
existingDBs[path] = &existingDBEntry{DB: db, buckets: hashset.New()}
}
// Create a session store backed by the given BoltDB instance
store, err := boltstore.New(
db,
boltstore.Config{DBOptions: boltstore.Options{BucketName: []byte(bucket)}},
[]byte(secureCookieKeyPair),
)
if err != nil {
return nil, err
}
existingDBs[path].buckets.Add(bucket)
// Invoke a reaper which checks and removes expired sessions periodically
quitC, doneC := reaper.Run(db, reaper.Options{BucketName: []byte(bucket)})
return &boltDBSessionStore{
Store: store,
DB: db,
doneC: doneC,
quitC: quitC,
}, nil
}
func (bsc *boltDBSessionStore) Close() error {
reaper.Quit(bsc.quitC, bsc.doneC)
return bsc.DB.Close()
}