git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add JWT session loader middleware
commitc81a7ed1976a7e79c1a70ebb10091fe503bb05b6 authorJoel Speed <joel.speed@hotmail.co.uk> dateSat 4 Jul 2020, 19:19 (6 years ago) parent2768321
3 files changed+742
pkg/apis/middleware/session.go+24 pkg/middleware/jwt_session.go+186 pkg/middleware/jwt_session_test.go+532
pkg/apis/middleware/session.gonew file+24
@@ -0,0 +1,24 @@
1+package middleware
2+
3+import (
4+ "context"
5+
6+ "github.com/coreos/go-oidc"
7+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
8+)
9+
10+// TokenToSessionFunc takes a rawIDToken and an idToken and converts it into a
11+// SessionState.
12+type TokenToSessionFunc func(ctx context.Context, rawIDToken string, idToken *oidc.IDToken) (*sessionsapi.SessionState, error)
13+
14+// TokenToSessionLoader pairs a token verifier with the correct converter function
15+// to convert the ID Token to a SessionState.
16+type TokenToSessionLoader struct {
17+ // Verfier is used to verify that the ID Token was signed by the claimed issuer
18+ // and that the token has not been tampered with.
19+ Verifier *oidc.IDTokenVerifier
20+
21+ // TokenToSession converts a rawIDToken and an idToken to a SessionState.
22+ // (Optional) If not set a default basic implementation is used.
23+ TokenToSession TokenToSessionFunc
24+}
pkg/middleware/jwt_session.gonew file+186
@@ -0,0 +1,186 @@
1+package middleware
2+
3+import (
4+ "context"
5+ "encoding/base64"
6+ "fmt"
7+ "net/http"
8+ "regexp"
9+ "strings"
10+
11+ "github.com/coreos/go-oidc"
12+ "github.com/justinas/alice"
13+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/middleware"
14+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
15+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
16+)
17+
18+const jwtRegexFormat = `^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`
19+
20+func NewJwtSessionLoader(sessionLoaders []middlewareapi.TokenToSessionLoader) alice.Constructor {
21+ for i, loader := range sessionLoaders {
22+ if loader.TokenToSession == nil {
23+ sessionLoaders[i] = middlewareapi.TokenToSessionLoader{
24+ Verifier: loader.Verifier,
25+ TokenToSession: createSessionStateFromBearerToken,
26+ }
27+ }
28+ }
29+
30+ js := &jwtSessionLoader{
31+ jwtRegex: regexp.MustCompile(jwtRegexFormat),
32+ sessionLoaders: sessionLoaders,
33+ }
34+ return js.loadSession
35+}
36+
37+// jwtSessionLoader is responsible for loading sessions from JWTs in
38+// Authorization headers.
39+type jwtSessionLoader struct {
40+ jwtRegex *regexp.Regexp
41+ sessionLoaders []middlewareapi.TokenToSessionLoader
42+}
43+
44+// loadSession attempts to load a session from a JWT stored in an Authorization
45+// header within the request.
46+// If no authorization header is found, or the header is invalid, no session
47+// will be loaded and the request will be passed to the next handler.
48+// If a session was loaded by a previous handler, it will not be replaced.
49+func (j *jwtSessionLoader) loadSession(next http.Handler) http.Handler {
50+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
51+ scope := GetRequestScope(req)
52+ // If scope is nil, this will panic.
53+ // A scope should always be injected before this handler is called.
54+ if scope.Session != nil {
55+ // The session was already loaded, pass to the next handler
56+ next.ServeHTTP(rw, req)
57+ return
58+ }
59+
60+ session, err := j.getJwtSession(req)
61+ if err != nil {
62+ logger.Printf("Error retrieving session from token in Authorization header: %v", err)
63+ }
64+
65+ // Add the session to the scope if it was found
66+ scope.Session = session
67+ next.ServeHTTP(rw, req)
68+ })
69+}
70+
71+// getJwtSession loads a session based on a JWT token in the authorization header.
72+// (see the config options skip-jwt-bearer-tokens and extra-jwt-issuers)
73+func (j *jwtSessionLoader) getJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
74+ auth := req.Header.Get("Authorization")
75+ if auth == "" {
76+ // No auth header provided, so don't attempt to load a session
77+ return nil, nil
78+ }
79+
80+ rawBearerToken, err := j.findBearerTokenFromHeader(auth)
81+ if err != nil {
82+ return nil, err
83+ }
84+
85+ for _, loader := range j.sessionLoaders {
86+ bearerToken, err := loader.Verifier.Verify(req.Context(), rawBearerToken)
87+ if err == nil {
88+ // The token was verified, convert it to a session
89+ return loader.TokenToSession(req.Context(), rawBearerToken, bearerToken)
90+ }
91+ }
92+
93+ return nil, fmt.Errorf("unable to verify jwt token: %q", req.Header.Get("Authorization"))
94+}
95+
96+// findBearerTokenFromHeader finds a valid JWT token from the Authorization header of a given request.
97+func (j *jwtSessionLoader) findBearerTokenFromHeader(header string) (string, error) {
98+ tokenType, token, err := splitAuthHeader(header)
99+ if err != nil {
100+ return "", err
101+ }
102+
103+ if tokenType == "Bearer" && j.jwtRegex.MatchString(token) {
104+ // Found a JWT as a bearer token
105+ return token, nil
106+ }
107+
108+ if tokenType == "Basic" {
109+ // Check if we have a Bearer token masquerading in Basic
110+ return j.getBasicToken(token)
111+ }
112+
113+ return "", fmt.Errorf("no valid bearer token found in authorization header")
114+}
115+
116+// getBasicToken tries to extract a token from the basic value provided.
117+func (j *jwtSessionLoader) getBasicToken(token string) (string, error) {
118+ b, err := base64.StdEncoding.DecodeString(token)
119+ if err != nil {
120+ return "", fmt.Errorf("invalid basic auth token: %v", err)
121+ }
122+
123+ pair := strings.SplitN(string(b), ":", 2)
124+ if len(pair) != 2 {
125+ return "", fmt.Errorf("invalid format: %q", b)
126+ }
127+ user, password := pair[0], pair[1]
128+
129+ // check user, user+password, or just password for a token
130+ if j.jwtRegex.MatchString(user) {
131+ // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
132+ if password == "" || password == "x-oauth-basic" {
133+ return user, nil
134+ }
135+ } else if j.jwtRegex.MatchString(password) {
136+ // support passwords and ignore user
137+ return password, nil
138+ }
139+
140+ return "", fmt.Errorf("invalid basic auth token found in authorization header")
141+}
142+
143+// splitAuthHeader takes the auth header value and splits it into the token type
144+// and the token value.
145+func splitAuthHeader(header string) (string, string, error) {
146+ s := strings.Split(header, " ")
147+ if len(s) != 2 {
148+ return "", "", fmt.Errorf("invalid authorization header: %q", header)
149+ }
150+ return s[0], s[1], nil
151+}
152+
153+// createSessionStateFromBearerToken is a default implementation for converting
154+// a JWT into a session state.
155+func createSessionStateFromBearerToken(ctx context.Context, rawIDToken string, idToken *oidc.IDToken) (*sessionsapi.SessionState, error) {
156+ var claims struct {
157+ Subject string `json:"sub"`
158+ Email string `json:"email"`
159+ Verified *bool `json:"email_verified"`
160+ PreferredUsername string `json:"preferred_username"`
161+ }
162+
163+ if err := idToken.Claims(&claims); err != nil {
164+ return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
165+ }
166+
167+ if claims.Email == "" {
168+ claims.Email = claims.Subject
169+ }
170+
171+ if claims.Verified != nil && !*claims.Verified {
172+ return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
173+ }
174+
175+ newSession := &sessionsapi.SessionState{
176+ Email: claims.Email,
177+ User: claims.Subject,
178+ PreferredUsername: claims.PreferredUsername,
179+ AccessToken: rawIDToken,
180+ IDToken: rawIDToken,
181+ RefreshToken: "",
182+ ExpiresOn: &idToken.Expiry,
183+ }
184+
185+ return newSession, nil
186+}
pkg/middleware/jwt_session_test.gonew file+532
@@ -0,0 +1,532 @@
1+package middleware
2+
3+import (
4+ "context"
5+ "crypto/rand"
6+ "crypto/rsa"
7+ "encoding/base64"
8+ "errors"
9+ "fmt"
10+ "net/http"
11+ "net/http/httptest"
12+ "regexp"
13+ "strings"
14+ "time"
15+
16+ "github.com/coreos/go-oidc"
17+ "github.com/dgrijalva/jwt-go"
18+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/middleware"
19+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
20+ . "github.com/onsi/ginkgo"
21+ . "github.com/onsi/ginkgo/extensions/table"
22+ . "github.com/onsi/gomega"
23+)
24+
25+type noOpKeySet struct {
26+}
27+
28+func (noOpKeySet) VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) {
29+ splitStrings := strings.Split(jwt, ".")
30+ payloadString := splitStrings[1]
31+ return base64.RawURLEncoding.DecodeString(payloadString)
32+}
33+
34+var _ = Describe("JWT Session Suite", func() {
35+ /* token payload:
36+ {
37+ "sub": "1234567890",
38+ "aud": "https://test.myapp.com",
39+ "name": "John Doe",
40+ "email": "john@example.com",
41+ "iss": "https://issuer.example.com",
42+ "iat": 1553691215,
43+ "exp": 1912151821
44+ }
45+ */
46+ const verifiedToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." +
47+ "eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly90ZXN0Lm15YXBwLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImVtY" +
48+ "WlsIjoiam9obkBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNTUzNjkxMj" +
49+ "E1LCJleHAiOjE5MTIxNTE4MjF9." +
50+ "rLVyzOnEldUq_pNkfa-WiV8TVJYWyZCaM2Am_uo8FGg11zD7l-qmz3x1seTvqpH6Y0Ty00fmv6dJnGnC8WMnPXQiodRTfhBSe" +
51+ "OKZMu0HkMD2sg52zlKkbfLTO6ic5VnbVgwjjrB8am_Ta6w7kyFUaB5C1BsIrrLMldkWEhynbb8"
52+
53+ const verifiedTokenXOAuthBasicBase64 = `ZXlKaGJHY2lPaUpTVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnpkV0lpT2lJeE1qTTBOVFkz
54+T0Rrd0lpd2lZWFZrSWpvaWFIUjBjSE02THk5MFpYTjBMbTE1WVhCd0xtTnZiU0lzSW01aGJXVWlP
55+aUpLYjJodUlFUnZaU0lzSW1WdFlXbHNJam9pYW05b2JrQmxlR0Z0Y0d4bExtTnZiU0lzSW1semN5
56+STZJbWgwZEhCek9pOHZhWE56ZFdWeUxtVjRZVzF3YkdVdVkyOXRJaXdpYVdGMElqb3hOVFV6Tmpr
57+eE1qRTFMQ0psZUhBaU9qRTVNVEl4TlRFNE1qRjkuckxWeXpPbkVsZFVxX3BOa2ZhLVdpVjhUVkpZ
58+V3laQ2FNMkFtX3VvOEZHZzExekQ3bC1xbXozeDFzZVR2cXBINlkwVHkwMGZtdjZkSm5HbkM4V01u
59+UFhRaW9kUlRmaEJTZU9LWk11MEhrTUQyc2c1MnpsS2tiZkxUTzZpYzVWbmJWZ3dqanJCOGFtX1Rh
60+Nnc3a3lGVWFCNUMxQnNJcnJMTWxka1dFaHluYmI4Ongtb2F1dGgtYmFzaWM=`
61+
62+ var verifiedSessionExpiry = time.Unix(1912151821, 0)
63+ var verifiedSession = &sessionsapi.SessionState{
64+ AccessToken: verifiedToken,
65+ IDToken: verifiedToken,
66+ Email: "john@example.com",
67+ User: "1234567890",
68+ ExpiresOn: &verifiedSessionExpiry,
69+ }
70+
71+ // validToken will pass the token regex so can be used to check token fetching
72+ // is valid. It will not pass the OIDC Verifier however.
73+ const validToken = "eyJfoobar.eyJfoobar.12345asdf"
74+
75+ Context("JwtSessionLoader", func() {
76+ var verifier *oidc.IDTokenVerifier
77+ const nonVerifiedToken = validToken
78+
79+ BeforeEach(func() {
80+ keyset := noOpKeySet{}
81+ verifier = oidc.NewVerifier("https://issuer.example.com", keyset,
82+ &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
83+ })
84+
85+ type jwtSessionLoaderTableInput struct {
86+ authorizationHeader string
87+ existingSession *sessionsapi.SessionState
88+ expectedSession *sessionsapi.SessionState
89+ }
90+
91+ DescribeTable("with an authorization header",
92+ func(in jwtSessionLoaderTableInput) {
93+ scope := &middlewareapi.RequestScope{
94+ Session: in.existingSession,
95+ }
96+
97+ // Set up the request with the authorization header and a request scope
98+ req := httptest.NewRequest("", "/", nil)
99+ req.Header.Set("Authorization", in.authorizationHeader)
100+ contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
101+ req = req.WithContext(contextWithScope)
102+
103+ rw := httptest.NewRecorder()
104+
105+ sessionLoaders := []middlewareapi.TokenToSessionLoader{
106+ {
107+ Verifier: verifier,
108+ },
109+ }
110+
111+ // Create the handler with a next handler that will capture the session
112+ // from the scope
113+ var gotSession *sessionsapi.SessionState
114+ handler := NewJwtSessionLoader(sessionLoaders)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
115+ gotSession = r.Context().Value(requestScopeKey).(*middlewareapi.RequestScope).Session
116+ }))
117+ handler.ServeHTTP(rw, req)
118+
119+ Expect(gotSession).To(Equal(in.expectedSession))
120+ },
121+ Entry("<no value>", jwtSessionLoaderTableInput{
122+ authorizationHeader: "",
123+ existingSession: nil,
124+ expectedSession: nil,
125+ }),
126+ Entry("abcdef", jwtSessionLoaderTableInput{
127+ authorizationHeader: "abcdef",
128+ existingSession: nil,
129+ expectedSession: nil,
130+ }),
131+ Entry("abcdef (with existing session)", jwtSessionLoaderTableInput{
132+ authorizationHeader: "abcdef",
133+ existingSession: &sessionsapi.SessionState{User: "user"},
134+ expectedSession: &sessionsapi.SessionState{User: "user"},
135+ }),
136+ Entry("Bearer <verifiedToken>", jwtSessionLoaderTableInput{
137+ authorizationHeader: fmt.Sprintf("Bearer %s", verifiedToken),
138+ existingSession: nil,
139+ expectedSession: verifiedSession,
140+ }),
141+ Entry("Bearer <nonVerifiedToken>", jwtSessionLoaderTableInput{
142+ authorizationHeader: fmt.Sprintf("Bearer %s", nonVerifiedToken),
143+ existingSession: nil,
144+ expectedSession: nil,
145+ }),
146+ Entry("Bearer <verifiedToken> (with existing session)", jwtSessionLoaderTableInput{
147+ authorizationHeader: fmt.Sprintf("Bearer %s", verifiedToken),
148+ existingSession: &sessionsapi.SessionState{User: "user"},
149+ expectedSession: &sessionsapi.SessionState{User: "user"},
150+ }),
151+ Entry("Basic Base64(<nonVerifiedToken>:) (No password)", jwtSessionLoaderTableInput{
152+ authorizationHeader: "Basic ZXlKZm9vYmFyLmV5SmZvb2Jhci4xMjM0NWFzZGY6",
153+ existingSession: nil,
154+ expectedSession: nil,
155+ }),
156+ Entry("Basic Base64(<verifiedToken>:x-oauth-basic) (Sentinel password)", jwtSessionLoaderTableInput{
157+ authorizationHeader: fmt.Sprintf("Basic %s", verifiedTokenXOAuthBasicBase64),
158+ existingSession: nil,
159+ expectedSession: verifiedSession,
160+ }),
161+ )
162+
163+ })
164+
165+ Context("getJWTSession", func() {
166+ var j *jwtSessionLoader
167+ const nonVerifiedToken = validToken
168+
169+ BeforeEach(func() {
170+ keyset := noOpKeySet{}
171+ verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
172+ &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
173+
174+ j = &jwtSessionLoader{
175+ jwtRegex: regexp.MustCompile(jwtRegexFormat),
176+ sessionLoaders: []middlewareapi.TokenToSessionLoader{
177+ {
178+ Verifier: verifier,
179+ TokenToSession: createSessionStateFromBearerToken,
180+ },
181+ },
182+ }
183+ })
184+
185+ type getJWTSessionTableInput struct {
186+ authorizationHeader string
187+ expectedErr error
188+ expectedSession *sessionsapi.SessionState
189+ }
190+
191+ DescribeTable("with an authorization header",
192+ func(in getJWTSessionTableInput) {
193+ req := httptest.NewRequest("", "/", nil)
194+ req.Header.Set("Authorization", in.authorizationHeader)
195+
196+ session, err := j.getJwtSession(req)
197+ if in.expectedErr != nil {
198+ Expect(err).To(MatchError(in.expectedErr))
199+ } else {
200+ Expect(err).ToNot(HaveOccurred())
201+ }
202+ Expect(session).To(Equal(in.expectedSession))
203+ },
204+ Entry("<no value>", getJWTSessionTableInput{
205+ authorizationHeader: "",
206+ expectedErr: nil,
207+ expectedSession: nil,
208+ }),
209+ Entry("abcdef", getJWTSessionTableInput{
210+ authorizationHeader: "abcdef",
211+ expectedErr: errors.New("invalid authorization header: \"abcdef\""),
212+ expectedSession: nil,
213+ }),
214+ Entry("Bearer abcdef", getJWTSessionTableInput{
215+ authorizationHeader: "Bearer abcdef",
216+ expectedErr: errors.New("no valid bearer token found in authorization header"),
217+ expectedSession: nil,
218+ }),
219+ Entry("Bearer <nonVerifiedToken>", getJWTSessionTableInput{
220+ authorizationHeader: fmt.Sprintf("Bearer %s", nonVerifiedToken),
221+ expectedErr: errors.New("unable to verify jwt token: \"Bearer eyJfoobar.eyJfoobar.12345asdf\""),
222+ expectedSession: nil,
223+ }),
224+ Entry("Bearer <verifiedToken>", getJWTSessionTableInput{
225+ authorizationHeader: fmt.Sprintf("Bearer %s", verifiedToken),
226+ expectedErr: nil,
227+ expectedSession: verifiedSession,
228+ }),
229+ Entry("Basic Base64(<nonVerifiedToken>:) (No password)", getJWTSessionTableInput{
230+ authorizationHeader: "Basic ZXlKZm9vYmFyLmV5SmZvb2Jhci4xMjM0NWFzZGY6",
231+ expectedErr: errors.New("unable to verify jwt token: \"Basic ZXlKZm9vYmFyLmV5SmZvb2Jhci4xMjM0NWFzZGY6\""),
232+ expectedSession: nil,
233+ }),
234+ Entry("Basic Base64(<verifiedToken>:x-oauth-basic) (Sentinel password)", getJWTSessionTableInput{
235+ authorizationHeader: fmt.Sprintf("Basic %s", verifiedTokenXOAuthBasicBase64),
236+ expectedErr: nil,
237+ expectedSession: verifiedSession,
238+ }),
239+ )
240+ })
241+
242+ Context("findBearerTokenFromHeader", func() {
243+ var j *jwtSessionLoader
244+
245+ BeforeEach(func() {
246+ j = &jwtSessionLoader{
247+ jwtRegex: regexp.MustCompile(jwtRegexFormat),
248+ }
249+ })
250+
251+ type findBearerTokenFromHeaderTableInput struct {
252+ header string
253+ expectedErr error
254+ expectedToken string
255+ }
256+
257+ DescribeTable("with a header",
258+ func(in findBearerTokenFromHeaderTableInput) {
259+ token, err := j.findBearerTokenFromHeader(in.header)
260+ if in.expectedErr != nil {
261+ Expect(err).To(MatchError(in.expectedErr))
262+ } else {
263+ Expect(err).ToNot(HaveOccurred())
264+ }
265+ Expect(token).To(Equal(in.expectedToken))
266+ },
267+ Entry("Bearer", findBearerTokenFromHeaderTableInput{
268+ header: "Bearer",
269+ expectedErr: errors.New("invalid authorization header: \"Bearer\""),
270+ expectedToken: "",
271+ }),
272+ Entry("Bearer abc def", findBearerTokenFromHeaderTableInput{
273+ header: "Bearer abc def",
274+ expectedErr: errors.New("invalid authorization header: \"Bearer abc def\""),
275+ expectedToken: "",
276+ }),
277+ Entry("Bearer abcdef", findBearerTokenFromHeaderTableInput{
278+ header: "Bearer abcdef",
279+ expectedErr: errors.New("no valid bearer token found in authorization header"),
280+ expectedToken: "",
281+ }),
282+ Entry("Bearer <valid-token>", findBearerTokenFromHeaderTableInput{
283+ header: fmt.Sprintf("Bearer %s", validToken),
284+ expectedErr: nil,
285+ expectedToken: validToken,
286+ }),
287+ Entry("Basic invalid-base64", findBearerTokenFromHeaderTableInput{
288+ header: "Basic invalid-base64",
289+ expectedErr: errors.New("invalid basic auth token: illegal base64 data at input byte 7"),
290+ expectedToken: "",
291+ }),
292+ Entry("Basic Base64(<validToken>:) (No password)", findBearerTokenFromHeaderTableInput{
293+ header: "Basic ZXlKZm9vYmFyLmV5SmZvb2Jhci4xMjM0NWFzZGY6",
294+ expectedErr: nil,
295+ expectedToken: validToken,
296+ }),
297+ Entry("Basic Base64(<validToken>:x-oauth-basic) (Sentinel password)", findBearerTokenFromHeaderTableInput{
298+ header: "Basic ZXlKZm9vYmFyLmV5SmZvb2Jhci4xMjM0NWFzZGY6eC1vYXV0aC1iYXNpYw==",
299+ expectedErr: nil,
300+ expectedToken: validToken,
301+ }),
302+ Entry("Basic Base64(any-user:<validToken>) (Matching password)", findBearerTokenFromHeaderTableInput{
303+ header: "Basic YW55LXVzZXI6ZXlKZm9vYmFyLmV5SmZvb2Jhci4xMjM0NWFzZGY=",
304+ expectedErr: nil,
305+ expectedToken: validToken,
306+ }),
307+ Entry("Basic Base64(any-user:any-password) (No matches)", findBearerTokenFromHeaderTableInput{
308+ header: "Basic YW55LXVzZXI6YW55LXBhc3N3b3Jk",
309+ expectedErr: errors.New("invalid basic auth token found in authorization header"),
310+ expectedToken: "",
311+ }),
312+ Entry("Basic Base64(any-user any-password) (Invalid format)", findBearerTokenFromHeaderTableInput{
313+ header: "Basic YW55LXVzZXIgYW55LXBhc3N3b3Jk",
314+ expectedErr: errors.New("invalid format: \"any-user any-password\""),
315+ expectedToken: "",
316+ }),
317+ Entry("Something <valid-token>", findBearerTokenFromHeaderTableInput{
318+ header: fmt.Sprintf("Something %s", validToken),
319+ expectedErr: errors.New("no valid bearer token found in authorization header"),
320+ expectedToken: "",
321+ }),
322+ )
323+
324+ })
325+
326+ Context("getBasicToken", func() {
327+ var j *jwtSessionLoader
328+
329+ BeforeEach(func() {
330+ j = &jwtSessionLoader{
331+ jwtRegex: regexp.MustCompile(jwtRegexFormat),
332+ }
333+ })
334+
335+ type getBasicTokenTableInput struct {
336+ token string
337+ expectedErr error
338+ expectedToken string
339+ }
340+
341+ DescribeTable("with a token",
342+ func(in getBasicTokenTableInput) {
343+ token, err := j.getBasicToken(in.token)
344+ if in.expectedErr != nil {
345+ Expect(err).To(MatchError(in.expectedErr))
346+ } else {
347+ Expect(err).ToNot(HaveOccurred())
348+ }
349+ Expect(token).To(Equal(in.expectedToken))
350+ },
351+ Entry("invalid-base64", getBasicTokenTableInput{
352+ token: "invalid-base64",
353+ expectedErr: errors.New("invalid basic auth token: illegal base64 data at input byte 7"),
354+ expectedToken: "",
355+ }),
356+ Entry("Base64(<validToken>:) (No password)", getBasicTokenTableInput{
357+ token: "ZXlKZm9vYmFyLmV5SmZvb2Jhci4xMjM0NWFzZGY6",
358+ expectedErr: nil,
359+ expectedToken: validToken,
360+ }),
361+ Entry("Base64(<validToken>:x-oauth-basic) (Sentinel password)", getBasicTokenTableInput{
362+ token: "ZXlKZm9vYmFyLmV5SmZvb2Jhci4xMjM0NWFzZGY6eC1vYXV0aC1iYXNpYw==",
363+ expectedErr: nil,
364+ expectedToken: validToken,
365+ }),
366+ Entry("Base64(any-user:<validToken>) (Matching password)", getBasicTokenTableInput{
367+ token: "YW55LXVzZXI6ZXlKZm9vYmFyLmV5SmZvb2Jhci4xMjM0NWFzZGY=",
368+ expectedErr: nil,
369+ expectedToken: validToken,
370+ }),
371+ Entry("Base64(any-user:any-password) (No matches)", getBasicTokenTableInput{
372+ token: "YW55LXVzZXI6YW55LXBhc3N3b3Jk",
373+ expectedErr: errors.New("invalid basic auth token found in authorization header"),
374+ expectedToken: "",
375+ }),
376+ Entry("Base64(any-user any-password) (Invalid format)", getBasicTokenTableInput{
377+ token: "YW55LXVzZXIgYW55LXBhc3N3b3Jk",
378+ expectedErr: errors.New("invalid format: \"any-user any-password\""),
379+ expectedToken: "",
380+ }),
381+ )
382+ })
383+
384+ Context("splitAuthHeader", func() {
385+ type splitAuthTableInput struct {
386+ header string
387+ expectedErr error
388+ expectedTokenType string
389+ expectedTokenValue string
390+ }
391+
392+ DescribeTable("with a header value",
393+ func(in splitAuthTableInput) {
394+ tt, tv, err := splitAuthHeader(in.header)
395+ if in.expectedErr != nil {
396+ Expect(err).To(MatchError(in.expectedErr))
397+ } else {
398+ Expect(err).ToNot(HaveOccurred())
399+ }
400+ Expect(tt).To(Equal(in.expectedTokenType))
401+ Expect(tv).To(Equal(in.expectedTokenValue))
402+ },
403+ Entry("Bearer abcdef", splitAuthTableInput{
404+ header: "Bearer abcdef",
405+ expectedErr: nil,
406+ expectedTokenType: "Bearer",
407+ expectedTokenValue: "abcdef",
408+ }),
409+ Entry("Bearer", splitAuthTableInput{
410+ header: "Bearer",
411+ expectedErr: errors.New("invalid authorization header: \"Bearer\""),
412+ expectedTokenType: "",
413+ expectedTokenValue: "",
414+ }),
415+ Entry("Bearer abc def", splitAuthTableInput{
416+ header: "Bearer abc def",
417+ expectedErr: errors.New("invalid authorization header: \"Bearer abc def\""),
418+ expectedTokenType: "",
419+ expectedTokenValue: "",
420+ }),
421+ )
422+ })
423+
424+ Context("createSessionStateFromBearerToken", func() {
425+ ctx := context.Background()
426+ expiresFuture := time.Now().Add(time.Duration(5) * time.Minute)
427+ verified := true
428+ notVerified := false
429+
430+ type idTokenClaims struct {
431+ Email string `json:"email,omitempty"`
432+ Verified *bool `json:"email_verified,omitempty"`
433+ jwt.StandardClaims
434+ }
435+
436+ type createSessionStateTableInput struct {
437+ idToken idTokenClaims
438+ expectedErr error
439+ expectedUser string
440+ expectedEmail string
441+ expectedExpires *time.Time
442+ }
443+
444+ DescribeTable("when creating a session from an IDToken",
445+ func(in createSessionStateTableInput) {
446+ verifier := oidc.NewVerifier(
447+ "https://issuer.example.com",
448+ noOpKeySet{},
449+ &oidc.Config{ClientID: "asdf1234"},
450+ )
451+
452+ key, err := rsa.GenerateKey(rand.Reader, 2048)
453+ Expect(err).ToNot(HaveOccurred())
454+
455+ rawIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS256, in.idToken).SignedString(key)
456+ Expect(err).ToNot(HaveOccurred())
457+
458+ // Pass to a dummy Verifier to get an oidc.IDToken from the rawIDToken for our actual test below
459+ idToken, err := verifier.Verify(context.Background(), rawIDToken)
460+ Expect(err).ToNot(HaveOccurred())
461+
462+ session, err := createSessionStateFromBearerToken(ctx, rawIDToken, idToken)
463+ if in.expectedErr != nil {
464+ Expect(err).To(MatchError(in.expectedErr))
465+ Expect(session).To(BeNil())
466+ return
467+ }
468+
469+ Expect(err).ToNot(HaveOccurred())
470+ Expect(session.AccessToken).To(Equal(rawIDToken))
471+ Expect(session.IDToken).To(Equal(rawIDToken))
472+ Expect(session.User).To(Equal(in.expectedUser))
473+ Expect(session.Email).To(Equal(in.expectedEmail))
474+ Expect(session.ExpiresOn.Unix()).To(Equal(in.expectedExpires.Unix()))
475+ Expect(session.RefreshToken).To(BeEmpty())
476+ Expect(session.PreferredUsername).To(BeEmpty())
477+ },
478+ Entry("with no email", createSessionStateTableInput{
479+ idToken: idTokenClaims{
480+ StandardClaims: jwt.StandardClaims{
481+ Audience: "asdf1234",
482+ ExpiresAt: expiresFuture.Unix(),
483+ Id: "id-some-id",
484+ IssuedAt: time.Now().Unix(),
485+ Issuer: "https://issuer.example.com",
486+ NotBefore: 0,
487+ Subject: "123456789",
488+ },
489+ },
490+ expectedErr: nil,
491+ expectedUser: "123456789",
492+ expectedEmail: "123456789",
493+ expectedExpires: &expiresFuture,
494+ }),
495+ Entry("with a verified email", createSessionStateTableInput{
496+ idToken: idTokenClaims{
497+ StandardClaims: jwt.StandardClaims{
498+ Audience: "asdf1234",
499+ ExpiresAt: expiresFuture.Unix(),
500+ Id: "id-some-id",
501+ IssuedAt: time.Now().Unix(),
502+ Issuer: "https://issuer.example.com",
503+ NotBefore: 0,
504+ Subject: "123456789",
505+ },
506+ Email: "foo@example.com",
507+ Verified: &verified,
508+ },
509+ expectedErr: nil,
510+ expectedUser: "123456789",
511+ expectedEmail: "foo@example.com",
512+ expectedExpires: &expiresFuture,
513+ }),
514+ Entry("with a non-verified email", createSessionStateTableInput{
515+ idToken: idTokenClaims{
516+ StandardClaims: jwt.StandardClaims{
517+ Audience: "asdf1234",
518+ ExpiresAt: expiresFuture.Unix(),
519+ Id: "id-some-id",
520+ IssuedAt: time.Now().Unix(),
521+ Issuer: "https://issuer.example.com",
522+ NotBefore: 0,
523+ Subject: "123456789",
524+ },
525+ Email: "foo@example.com",
526+ Verified: &notVerified,
527+ },
528+ expectedErr: errors.New("email in id_token (foo@example.com) isn't verified"),
529+ }),
530+ )
531+ })
532+})